From e3c0e019343fd7378cf21af33f882c17ec9b6a25 Mon Sep 17 00:00:00 2001 From: slarticodefast <161409025+slarticodefast@users.noreply.github.com> Date: Tue, 18 Nov 2025 19:09:49 +0100 Subject: [PATCH] Cleanup TipsSystem, add TippyOnTrigger (#41477) * tippy on trigger and refactor * optional parameter for command --- Content.Client/Tips/TipsSystem.cs | 5 + .../Administration/Commands/TippyCommand.cs | 103 +++++++++ Content.Server/Tips/TipsSystem.cs | 211 +++++------------- Content.Shared/Tips/SharedTipsSystem.cs | 74 ++++++ Content.Shared/Tips/TippyEvent.cs | 38 +++- .../Effects/TippyOnTriggerComponent.cs | 66 ++++++ .../Trigger/Systems/TippyOnTriggerSystem.cs | 49 ++++ .../Locale/en-US/commands/tippy-command.ftl | 2 +- Resources/engineCommandPerms.yml | 5 - 9 files changed, 376 insertions(+), 177 deletions(-) create mode 100644 Content.Client/Tips/TipsSystem.cs create mode 100644 Content.Server/Administration/Commands/TippyCommand.cs create mode 100644 Content.Shared/Tips/SharedTipsSystem.cs create mode 100644 Content.Shared/Trigger/Components/Effects/TippyOnTriggerComponent.cs create mode 100644 Content.Shared/Trigger/Systems/TippyOnTriggerSystem.cs diff --git a/Content.Client/Tips/TipsSystem.cs b/Content.Client/Tips/TipsSystem.cs new file mode 100644 index 0000000000..00fc08437c --- /dev/null +++ b/Content.Client/Tips/TipsSystem.cs @@ -0,0 +1,5 @@ +using Content.Shared.Tips; + +namespace Content.Client.Tips; + +public sealed class TipsSystem : SharedTipsSystem; diff --git a/Content.Server/Administration/Commands/TippyCommand.cs b/Content.Server/Administration/Commands/TippyCommand.cs new file mode 100644 index 0000000000..b79c160706 --- /dev/null +++ b/Content.Server/Administration/Commands/TippyCommand.cs @@ -0,0 +1,103 @@ +using Content.Shared.Administration; +using Content.Shared.Tips; +using Robust.Server.Player; +using Robust.Shared.Console; +using Robust.Shared.Player; +using Robust.Shared.Prototypes; + +namespace Content.Server.Administration.Commands; + +[AdminCommand(AdminFlags.Fun)] +public sealed class TippyCommand : LocalizedEntityCommands +{ + [Dependency] private readonly SharedTipsSystem _tips = default!; + [Dependency] private readonly IPrototypeManager _prototype = default!; + [Dependency] private readonly IPlayerManager _player = default!; + + public override string Command => "tippy"; + + public override void Execute(IConsoleShell shell, string argStr, string[] args) + { + if (args.Length < 2) + { + shell.WriteLine(Loc.GetString("cmd-tippy-help")); + return; + } + + ICommonSession? targetSession = null; + if (args[0] != "all") + { + if (!_player.TryGetSessionByUsername(args[0], out targetSession)) + { + shell.WriteLine(Loc.GetString("cmd-tippy-error-no-user")); + return; + } + } + + var msg = args[1]; + + EntProtoId? prototype = null; + if (args.Length > 2) + { + if (args[2] == "null") + prototype = null; + else if (!_prototype.HasIndex(args[2])) + { + shell.WriteError(Loc.GetString("cmd-tippy-error-no-prototype", ("proto", args[2]))); + return; + } + else + prototype = args[2]; + } + + var speakTime = _tips.GetSpeechTime(msg); + var slideTime = 3f; + var waddleInterval = 0.5f; + + if (args.Length > 3 && float.TryParse(args[3], out var parsedSpeakTime)) + speakTime = parsedSpeakTime; + + if (args.Length > 4 && float.TryParse(args[4], out var parsedSlideTime)) + slideTime = parsedSlideTime; + + if (args.Length > 5 && float.TryParse(args[5], out var parsedWaddleInterval)) + waddleInterval = parsedWaddleInterval; + + if (targetSession != null) // send to specified player + _tips.SendTippy(targetSession, msg, prototype, speakTime, slideTime, waddleInterval); + else // send to everyone + _tips.SendTippy(msg, prototype, speakTime, slideTime, waddleInterval); + } + + public override CompletionResult GetCompletion(IConsoleShell shell, string[] args) + { + return args.Length switch + { + 1 => CompletionResult.FromHintOptions( + CompletionHelper.SessionNames(players: _player), + Loc.GetString("cmd-tippy-auto-1")), + 2 => CompletionResult.FromHint(Loc.GetString("cmd-tippy-auto-2")), + 3 => CompletionResult.FromHintOptions( + CompletionHelper.PrototypeIdsLimited(args[2], _prototype), + Loc.GetString("cmd-tippy-auto-3")), + 4 => CompletionResult.FromHint(Loc.GetString("cmd-tippy-auto-4")), + 5 => CompletionResult.FromHint(Loc.GetString("cmd-tippy-auto-5")), + 6 => CompletionResult.FromHint(Loc.GetString("cmd-tippy-auto-6")), + _ => CompletionResult.Empty + }; + } +} + +[AdminCommand(AdminFlags.Fun)] +public sealed class TipCommand : LocalizedEntityCommands +{ + [Dependency] private readonly SharedTipsSystem _tips = default!; + + public override string Command => "tip"; + + public override void Execute(IConsoleShell shell, string argStr, string[] args) + { + _tips.AnnounceRandomTip(); + _tips.RecalculateNextTipTime(); + } +} diff --git a/Content.Server/Tips/TipsSystem.cs b/Content.Server/Tips/TipsSystem.cs index cd7f7b52f5..406ee76ca6 100644 --- a/Content.Server/Tips/TipsSystem.cs +++ b/Content.Server/Tips/TipsSystem.cs @@ -4,10 +4,7 @@ using Content.Shared.CCVar; using Content.Shared.Chat; using Content.Shared.Dataset; using Content.Shared.Tips; -using Robust.Server.GameObjects; -using Robust.Server.Player; using Robust.Shared.Configuration; -using Robust.Shared.Console; using Robust.Shared.Player; using Robust.Shared.Prototypes; using Robust.Shared.Random; @@ -15,10 +12,7 @@ using Robust.Shared.Timing; namespace Content.Server.Tips; -/// -/// Handles periodically displaying gameplay tips to all players ingame. -/// -public sealed class TipsSystem : EntitySystem +public sealed class TipsSystem : SharedTipsSystem { [Dependency] private readonly IChatManager _chat = default!; [Dependency] private readonly IPrototypeManager _prototype = default!; @@ -26,8 +20,6 @@ public sealed class TipsSystem : EntitySystem [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly GameTicker _ticker = default!; - [Dependency] private readonly IConsoleHost _conHost = default!; - [Dependency] private readonly IPlayerManager _playerManager = default!; private bool _tipsEnabled; private float _tipTimeOutOfRound; @@ -35,16 +27,6 @@ public sealed class TipsSystem : EntitySystem private string _tipsDataset = ""; private float _tipTippyChance; - /// - /// Always adds this time to a speech message. This is so really short message stay around for a bit. - /// - private const float SpeechBuffer = 3f; - - /// - /// Expected reading speed. - /// - private const float Wpm = 180f; - [ViewVariables(VVAccess.ReadWrite)] private TimeSpan _nextTipTime = TimeSpan.Zero; @@ -53,110 +35,45 @@ public sealed class TipsSystem : EntitySystem base.Initialize(); SubscribeLocalEvent(OnGameRunLevelChanged); - Subs.CVar(_cfg, CCVars.TipFrequencyOutOfRound, SetOutOfRound, true); - Subs.CVar(_cfg, CCVars.TipFrequencyInRound, SetInRound, true); Subs.CVar(_cfg, CCVars.TipsEnabled, SetEnabled, true); - Subs.CVar(_cfg, CCVars.TipsDataset, SetDataset, true); - Subs.CVar(_cfg, CCVars.TipsTippyChance, SetTippyChance, true); + Subs.CVar(_cfg, CCVars.TipFrequencyOutOfRound, value => _tipTimeOutOfRound = value, true); + Subs.CVar(_cfg, CCVars.TipFrequencyInRound, value => _tipTimeInRound = value, true); + Subs.CVar(_cfg, CCVars.TipsDataset, value => _tipsDataset = value, true); + Subs.CVar(_cfg, CCVars.TipsTippyChance, value => _tipTippyChance = value, true); RecalculateNextTipTime(); - _conHost.RegisterCommand("tippy", Loc.GetString("cmd-tippy-desc"), Loc.GetString("cmd-tippy-help"), SendTippy, SendTippyHelper); - _conHost.RegisterCommand("tip", Loc.GetString("cmd-tip-desc"), "tip", SendTip); } - private CompletionResult SendTippyHelper(IConsoleShell shell, string[] args) + private void OnGameRunLevelChanged(GameRunLevelChangedEvent ev) { - return args.Length switch + // reset for lobby -> inround + // reset for inround -> post but not post -> lobby + if (ev.New == GameRunLevel.InRound || ev.Old == GameRunLevel.InRound) { - 1 => CompletionResult.FromHintOptions( - CompletionHelper.SessionNames(players: _playerManager), - Loc.GetString("cmd-tippy-auto-1")), - 2 => CompletionResult.FromHint(Loc.GetString("cmd-tippy-auto-2")), - 3 => CompletionResult.FromHintOptions( - CompletionHelper.PrototypeIdsLimited(args[2], _prototype), - Loc.GetString("cmd-tippy-auto-3")), - 4 => CompletionResult.FromHint(Loc.GetString("cmd-tippy-auto-4")), - 5 => CompletionResult.FromHint(Loc.GetString("cmd-tippy-auto-5")), - 6 => CompletionResult.FromHint(Loc.GetString("cmd-tippy-auto-6")), - _ => CompletionResult.Empty - }; + RecalculateNextTipTime(); + } } - private void SendTip(IConsoleShell shell, string argstr, string[] args) + private void SetEnabled(bool value) { - AnnounceRandomTip(); - RecalculateNextTipTime(); + _tipsEnabled = value; + + if (_nextTipTime != TimeSpan.Zero) + RecalculateNextTipTime(); } - private void SendTippy(IConsoleShell shell, string argstr, string[] args) + public override void RecalculateNextTipTime() { - if (args.Length < 2) + if (_ticker.RunLevel == GameRunLevel.InRound) { - shell.WriteLine(Loc.GetString("cmd-tippy-help")); - return; + _nextTipTime = _timing.CurTime + TimeSpan.FromSeconds(_tipTimeInRound); } - - ActorComponent? actor = null; - if (args[0] != "all") - { - ICommonSession? session; - if (args.Length > 0) - { - // Get player entity - if (!_playerManager.TryGetSessionByUsername(args[0], out session)) - { - shell.WriteLine(Loc.GetString("cmd-tippy-error-no-user")); - return; - } - } - else - { - session = shell.Player; - } - - if (session?.AttachedEntity is not { } user) - { - shell.WriteLine(Loc.GetString("cmd-tippy-error-no-user")); - return; - } - - if (!TryComp(user, out actor)) - { - shell.WriteError(Loc.GetString("cmd-tippy-error-no-user")); - return; - } - } - - var ev = new TippyEvent(args[1]); - - if (args.Length > 2) - { - ev.Proto = args[2]; - if (!_prototype.HasIndex(args[2])) - { - shell.WriteError(Loc.GetString("cmd-tippy-error-no-prototype", ("proto", args[2]))); - return; - } - } - - if (args.Length > 3) - ev.SpeakTime = float.Parse(args[3]); else - ev.SpeakTime = GetSpeechTime(ev.Msg); - - if (args.Length > 4) - ev.SlideTime = float.Parse(args[4]); - - if (args.Length > 5) - ev.WaddleInterval = float.Parse(args[5]); - - if (actor != null) - RaiseNetworkEvent(ev, actor.PlayerSession); - else - RaiseNetworkEvent(ev); + { + _nextTipTime = _timing.CurTime + TimeSpan.FromSeconds(_tipTimeOutOfRound); + } } - public override void Update(float frameTime) { base.Update(frameTime); @@ -171,41 +88,30 @@ public sealed class TipsSystem : EntitySystem } } - private void SetOutOfRound(float value) + public override void SendTippy( + string message, + EntProtoId? prototype = null, + float speakTime = 5f, + float slideTime = 3f, + float waddleInterval = 0.5f) { - _tipTimeOutOfRound = value; + var ev = new TippyEvent(message, prototype, speakTime, slideTime, waddleInterval); + RaiseNetworkEvent(ev); } - private void SetInRound(float value) + public override void SendTippy( + ICommonSession session, + string message, + EntProtoId? prototype = null, + float speakTime = 5f, + float slideTime = 3f, + float waddleInterval = 0.5f) { - _tipTimeInRound = value; + var ev = new TippyEvent(message, prototype, speakTime, slideTime, waddleInterval); + RaiseNetworkEvent(ev, session); } - private void SetEnabled(bool value) - { - _tipsEnabled = value; - - if (_nextTipTime != TimeSpan.Zero) - RecalculateNextTipTime(); - } - - private void SetDataset(string value) - { - _tipsDataset = value; - } - - private void SetTippyChance(float value) - { - _tipTippyChance = value; - } - - public static float GetSpeechTime(string text) - { - var wordCount = (float)text.Split().Length; - return SpeechBuffer + wordCount * (60f / Wpm); - } - - private void AnnounceRandomTip() + public override void AnnounceRandomTip() { if (!_prototype.TryIndex(_tipsDataset, out var tips)) return; @@ -215,35 +121,20 @@ public sealed class TipsSystem : EntitySystem if (_random.Prob(_tipTippyChance)) { - var ev = new TippyEvent(msg); - ev.SpeakTime = GetSpeechTime(msg); - RaiseNetworkEvent(ev); - } else - { - _chat.ChatMessageToManyFiltered(Filter.Broadcast(), ChatChannel.OOC, tip, msg, - EntityUid.Invalid, false, false, Color.MediumPurple); - } - } - - private void RecalculateNextTipTime() - { - if (_ticker.RunLevel == GameRunLevel.InRound) - { - _nextTipTime = _timing.CurTime + TimeSpan.FromSeconds(_tipTimeInRound); + var speakTime = GetSpeechTime(msg); + SendTippy(msg, speakTime: speakTime); } else { - _nextTipTime = _timing.CurTime + TimeSpan.FromSeconds(_tipTimeOutOfRound); - } - } - - private void OnGameRunLevelChanged(GameRunLevelChangedEvent ev) - { - // reset for lobby -> inround - // reset for inround -> post but not post -> lobby - if (ev.New == GameRunLevel.InRound || ev.Old == GameRunLevel.InRound) - { - RecalculateNextTipTime(); + _chat.ChatMessageToManyFiltered( + Filter.Broadcast(), + ChatChannel.OOC, + tip, + msg, + EntityUid.Invalid, + false, + false, + Color.MediumPurple); } } } diff --git a/Content.Shared/Tips/SharedTipsSystem.cs b/Content.Shared/Tips/SharedTipsSystem.cs new file mode 100644 index 0000000000..1973aae1a3 --- /dev/null +++ b/Content.Shared/Tips/SharedTipsSystem.cs @@ -0,0 +1,74 @@ +using Content.Shared.CCVar; +using Robust.Shared.Player; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Tips; + +/// +/// Handles periodically displaying gameplay tips to all players ingame. +/// +public abstract class SharedTipsSystem : EntitySystem +{ + /// + /// Always adds this time to a speech message. This is so really short message stay around for a bit. + /// + private const float SpeechBuffer = 3f; + + /// + /// Expected reading speed. + /// + private const float Wpm = 180f; + + /// + /// Send a tippy message to all clients. + /// + /// The text to show in the speech bubble. + /// The entity to show. Defaults to tippy. + /// The time the speech bubble is shown, in seconds. + /// The time the entity takes to walk onto the screen, in seconds. + /// The time between waddle animation steps, in seconds. + public virtual void SendTippy( + string message, + EntProtoId? prototype = null, + float speakTime = 5f, + float slideTime = 3f, + float waddleInterval = 0.5f) + { } + + /// + /// Send a tippy message to the given player session. + /// + /// The player session to send the message to. + /// The text to show in the speech bubble. + /// The entity to show. Defaults to tippy. + /// The time the speech bubble is shown, in seconds. + /// The time the entity takes to walk onto the screen, in seconds. + /// The time between waddle animation steps, in seconds. + public virtual void SendTippy( + ICommonSession session, + string message, + EntProtoId? prototype = null, + float speakTime = 5f, + float slideTime = 3f, + float waddleInterval = 0.5f) + { } + + /// + /// Send a random tippy message from the dataset given in . + /// + public virtual void AnnounceRandomTip() { } + + /// + /// Set a random time stamp for the next automatic game tip. + /// + public virtual void RecalculateNextTipTime() { } + + /// + /// Calculate the recommended speak time for a given message. + /// + public float GetSpeechTime(string text) + { + var wordCount = (float)text.Split().Length; + return SpeechBuffer + wordCount * (60f / Wpm); + } +} diff --git a/Content.Shared/Tips/TippyEvent.cs b/Content.Shared/Tips/TippyEvent.cs index e5dc0b7f35..c6b028aa31 100644 --- a/Content.Shared/Tips/TippyEvent.cs +++ b/Content.Shared/Tips/TippyEvent.cs @@ -1,21 +1,37 @@ +using Robust.Shared.Prototypes; using Robust.Shared.Serialization; namespace Content.Shared.Tips; +/// +/// Networked event that makes a client show a message on their screen using tippy or another protoype. +/// [Serializable, NetSerializable] -public sealed class TippyEvent : EntityEventArgs +public sealed class TippyEvent(string msg, EntProtoId? proto, float speakTime, float slideTime, float waddleInterval) : EntityEventArgs { - public TippyEvent(string msg) - { - Msg = msg; - } + /// + /// The text to show in the speech bubble. + /// + public string Msg = msg; - public string Msg; - public string? Proto; + /// + /// The entity to show. Defaults to tippy. + /// + public EntProtoId? Proto = proto; - // TODO: Why are these defaults even here, have the caller specify. This get overriden only most of the time. - public float SpeakTime = 5; - public float SlideTime = 3; - public float WaddleInterval = 0.5f; + /// + /// The time the speech bubble is shown, in seconds. + /// + public float SpeakTime = speakTime; + + /// + /// The time the entity takes to walk onto the screen, in seconds. + /// + public float SlideTime = slideTime; + + /// + /// The time between waddle animation steps, in seconds. + /// + public float WaddleInterval = waddleInterval; } diff --git a/Content.Shared/Trigger/Components/Effects/TippyOnTriggerComponent.cs b/Content.Shared/Trigger/Components/Effects/TippyOnTriggerComponent.cs new file mode 100644 index 0000000000..9476d9ea1c --- /dev/null +++ b/Content.Shared/Trigger/Components/Effects/TippyOnTriggerComponent.cs @@ -0,0 +1,66 @@ +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Trigger.Components.Effects; + +/// +/// Sends a tippy message to either the entity or all players when triggered. +/// If TargetUser is true the user will receive the message. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class TippyOnTriggerComponent : BaseXOnTriggerComponent +{ + /// + /// Unlocalized message text to send to the player(s). + /// Intended only for admeme purposes. For anything else you should use instead. + /// + [DataField, AutoNetworkedField] + public string Message = string.Empty; + + /// + /// Localized message text to send to the player(s). + /// This has priority over . + /// + [DataField, AutoNetworkedField] + public LocId? LocMessage; + + /// + /// If true the message will be send to all players. + /// If false it will be send to the user or owning entity, depending on . + /// + [DataField, AutoNetworkedField] + public bool SendToAll; + + /// + /// The entity prototype to show to the client. + /// Will default to tippy if null. + /// + [DataField, AutoNetworkedField] + public EntProtoId? Prototype; + + /// + /// Use the prototype of the entity owning this component? + /// Will take priority over . + /// + [DataField, AutoNetworkedField] + public bool UseOwnerPrototype; + + /// + /// The time the speech bubble is shown, in seconds. + /// Will be calculated automatically from the message length if null. + /// + [DataField, AutoNetworkedField] + public float? SpeakTime; + + /// + /// The time the entity takes to walk onto the screen, in seconds. + /// + [DataField, AutoNetworkedField] + public float SlideTime = 3f; + + /// + /// The time between waddle animation steps, in seconds. + /// + [DataField, AutoNetworkedField] + public float WaddleInterval = 0.5f; +} diff --git a/Content.Shared/Trigger/Systems/TippyOnTriggerSystem.cs b/Content.Shared/Trigger/Systems/TippyOnTriggerSystem.cs new file mode 100644 index 0000000000..10c55490af --- /dev/null +++ b/Content.Shared/Trigger/Systems/TippyOnTriggerSystem.cs @@ -0,0 +1,49 @@ +using Content.Shared.Tips; +using Content.Shared.Trigger.Components.Effects; +using Robust.Shared.Player; + +namespace Content.Shared.Trigger.Systems; + +public sealed class TippyOnTriggerSystem : EntitySystem +{ + [Dependency] private readonly SharedTipsSystem _tips = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnTrigger); + } + + private void OnTrigger(Entity ent, ref TriggerEvent args) + { + if (args.Key != null && !ent.Comp.KeysIn.Contains(args.Key)) + return; + + var msg = ent.Comp.Message; + var prototype = ent.Comp.Prototype; + + if (ent.Comp.LocMessage != null) + msg = Loc.GetString(ent.Comp.LocMessage.Value); + + if (ent.Comp.UseOwnerPrototype) + prototype = Prototype(ent)?.ID; + + var speakTime = ent.Comp.SpeakTime ?? _tips.GetSpeechTime(msg); + + if (ent.Comp.SendToAll) + { + _tips.SendTippy(msg, prototype, speakTime, ent.Comp.SlideTime, ent.Comp.WaddleInterval); + } + else + { + var target = ent.Comp.TargetUser ? args.User : ent.Owner; + if (!TryComp(target, out var actor)) + return; + + _tips.SendTippy(actor.PlayerSession, msg, prototype, speakTime, ent.Comp.SlideTime, ent.Comp.WaddleInterval); + } + + args.Handled = true; + } +} diff --git a/Resources/Locale/en-US/commands/tippy-command.ftl b/Resources/Locale/en-US/commands/tippy-command.ftl index 6b9a95a1dd..f814506009 100644 --- a/Resources/Locale/en-US/commands/tippy-command.ftl +++ b/Resources/Locale/en-US/commands/tippy-command.ftl @@ -1,5 +1,5 @@ cmd-tippy-desc = Broadcast a message as Tippy the clown. -cmd-tippy-help = tippy [entity prototype] [speak time] [slide time] [waddle interval] +cmd-tippy-help = tippy [entity prototype | null] [speak time] [slide time] [waddle interval] cmd-tippy-auto-1 = cmd-tippy-auto-2 = message cmd-tippy-auto-3 = entity prototype diff --git a/Resources/engineCommandPerms.yml b/Resources/engineCommandPerms.yml index 194d05d13d..4520a1da2d 100644 --- a/Resources/engineCommandPerms.yml +++ b/Resources/engineCommandPerms.yml @@ -96,11 +96,6 @@ Commands: - listplayers -- Flags: FUN - Commands: - - tippy - - tip - - Flags: SERVER Commands: - delete