--- /dev/null
+using Content.Shared.Tips;
+
+namespace Content.Client.Tips;
+
+public sealed class TipsSystem : SharedTipsSystem;
--- /dev/null
+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<EntityPrototype>(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<EntityPrototype>(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();
+ }
+}
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;
namespace Content.Server.Tips;
-/// <summary>
-/// Handles periodically displaying gameplay tips to all players ingame.
-/// </summary>
-public sealed class TipsSystem : EntitySystem
+public sealed class TipsSystem : SharedTipsSystem
{
[Dependency] private readonly IChatManager _chat = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
[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;
private string _tipsDataset = "";
private float _tipTippyChance;
- /// <summary>
- /// Always adds this time to a speech message. This is so really short message stay around for a bit.
- /// </summary>
- private const float SpeechBuffer = 3f;
-
- /// <summary>
- /// Expected reading speed.
- /// </summary>
- private const float Wpm = 180f;
-
[ViewVariables(VVAccess.ReadWrite)]
private TimeSpan _nextTipTime = TimeSpan.Zero;
base.Initialize();
SubscribeLocalEvent<GameRunLevelChangedEvent>(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<EntityPrototype>(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)
- {
- shell.WriteLine(Loc.GetString("cmd-tippy-help"));
- return;
- }
-
- ActorComponent? actor = null;
- if (args[0] != "all")
+ if (_ticker.RunLevel == GameRunLevel.InRound)
{
- 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;
- }
+ _nextTipTime = _timing.CurTime + TimeSpan.FromSeconds(_tipTimeInRound);
}
-
- var ev = new TippyEvent(args[1]);
-
- if (args.Length > 2)
+ else
{
- ev.Proto = args[2];
- if (!_prototype.HasIndex<EntityPrototype>(args[2]))
- {
- shell.WriteError(Loc.GetString("cmd-tippy-error-no-prototype", ("proto", args[2])));
- return;
- }
+ _nextTipTime = _timing.CurTime + TimeSpan.FromSeconds(_tipTimeOutOfRound);
}
-
- 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);
}
-
public override void Update(float frameTime)
{
base.Update(frameTime);
}
}
- 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<LocalizedDatasetPrototype>(_tipsDataset, out var tips))
return;
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);
}
}
}
--- /dev/null
+using Content.Shared.CCVar;
+using Robust.Shared.Player;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Tips;
+
+/// <summary>
+/// Handles periodically displaying gameplay tips to all players ingame.
+/// </summary>
+public abstract class SharedTipsSystem : EntitySystem
+{
+ /// <summary>
+ /// Always adds this time to a speech message. This is so really short message stay around for a bit.
+ /// </summary>
+ private const float SpeechBuffer = 3f;
+
+ /// <summary>
+ /// Expected reading speed.
+ /// </summary>
+ private const float Wpm = 180f;
+
+ /// <summary>
+ /// Send a tippy message to all clients.
+ /// </summary>
+ /// <param name="message">The text to show in the speech bubble.</param>
+ /// <param name="prototype">The entity to show. Defaults to tippy.</param>
+ /// <param name="speakTime">The time the speech bubble is shown, in seconds.</param>
+ /// <param name="slideTime">The time the entity takes to walk onto the screen, in seconds.</param>
+ /// <param name="waddleInterval">The time between waddle animation steps, in seconds.</param>
+ public virtual void SendTippy(
+ string message,
+ EntProtoId? prototype = null,
+ float speakTime = 5f,
+ float slideTime = 3f,
+ float waddleInterval = 0.5f)
+ { }
+
+ /// <summary>
+ /// Send a tippy message to the given player session.
+ /// </summary>
+ /// <param name="session">The player session to send the message to.</param>
+ /// <param name="message">The text to show in the speech bubble.</param>
+ /// <param name="prototype">The entity to show. Defaults to tippy.</param>
+ /// <param name="speakTime">The time the speech bubble is shown, in seconds.</param>
+ /// <param name="slideTime">The time the entity takes to walk onto the screen, in seconds.</param>
+ /// <param name="waddleInterval">The time between waddle animation steps, in seconds.</param>
+ public virtual void SendTippy(
+ ICommonSession session,
+ string message,
+ EntProtoId? prototype = null,
+ float speakTime = 5f,
+ float slideTime = 3f,
+ float waddleInterval = 0.5f)
+ { }
+
+ /// <summary>
+ /// Send a random tippy message from the dataset given in <see cref="CCVars.TipsDataset"/>.
+ /// </summary>
+ public virtual void AnnounceRandomTip() { }
+
+ /// <summary>
+ /// Set a random time stamp for the next automatic game tip.
+ /// </summary>
+ public virtual void RecalculateNextTipTime() { }
+
+ /// <summary>
+ /// Calculate the recommended speak time for a given message.
+ /// </summary>
+ public float GetSpeechTime(string text)
+ {
+ var wordCount = (float)text.Split().Length;
+ return SpeechBuffer + wordCount * (60f / Wpm);
+ }
+}
+using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
namespace Content.Shared.Tips;
+/// <summary>
+/// Networked event that makes a client show a message on their screen using tippy or another protoype.
+/// </summary>
[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;
- }
+ /// <summary>
+ /// The text to show in the speech bubble.
+ /// </summary>
+ public string Msg = msg;
- public string Msg;
- public string? Proto;
+ /// <summary>
+ /// The entity to show. Defaults to tippy.
+ /// </summary>
+ 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;
+ /// <summary>
+ /// The time the speech bubble is shown, in seconds.
+ /// </summary>
+ public float SpeakTime = speakTime;
+
+ /// <summary>
+ /// The time the entity takes to walk onto the screen, in seconds.
+ /// </summary>
+ public float SlideTime = slideTime;
+
+ /// <summary>
+ /// The time between waddle animation steps, in seconds.
+ /// </summary>
+ public float WaddleInterval = waddleInterval;
}
--- /dev/null
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Trigger.Components.Effects;
+
+/// <summary>
+/// Sends a tippy message to either the entity or all players when triggered.
+/// If TargetUser is true the user will receive the message.
+/// </summary>
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+public sealed partial class TippyOnTriggerComponent : BaseXOnTriggerComponent
+{
+ /// <summary>
+ /// Unlocalized message text to send to the player(s).
+ /// Intended only for admeme purposes. For anything else you should use <see cref="LocMessage"/> instead.
+ /// </summary>
+ [DataField, AutoNetworkedField]
+ public string Message = string.Empty;
+
+ /// <summary>
+ /// Localized message text to send to the player(s).
+ /// This has priority over <see cref="Message"/>.
+ /// </summary>
+ [DataField, AutoNetworkedField]
+ public LocId? LocMessage;
+
+ /// <summary>
+ /// If true the message will be send to all players.
+ /// If false it will be send to the user or owning entity, depending on <see cref="BaseXOnTriggerComponent.TargetUser"/>.
+ /// </summary>
+ [DataField, AutoNetworkedField]
+ public bool SendToAll;
+
+ /// <summary>
+ /// The entity prototype to show to the client.
+ /// Will default to tippy if null.
+ /// </summary>
+ [DataField, AutoNetworkedField]
+ public EntProtoId? Prototype;
+
+ /// <summary>
+ /// Use the prototype of the entity owning this component?
+ /// Will take priority over <see cref="Prototype"/>.
+ /// </summary>
+ [DataField, AutoNetworkedField]
+ public bool UseOwnerPrototype;
+
+ /// <summary>
+ /// The time the speech bubble is shown, in seconds.
+ /// Will be calculated automatically from the message length if null.
+ /// </summary>
+ [DataField, AutoNetworkedField]
+ public float? SpeakTime;
+
+ /// <summary>
+ /// The time the entity takes to walk onto the screen, in seconds.
+ /// </summary>
+ [DataField, AutoNetworkedField]
+ public float SlideTime = 3f;
+
+ /// <summary>
+ /// The time between waddle animation steps, in seconds.
+ /// </summary>
+ [DataField, AutoNetworkedField]
+ public float WaddleInterval = 0.5f;
+}
--- /dev/null
+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<TippyOnTriggerComponent, TriggerEvent>(OnTrigger);
+ }
+
+ private void OnTrigger(Entity<TippyOnTriggerComponent> 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<ActorComponent>(target, out var actor))
+ return;
+
+ _tips.SendTippy(actor.PlayerSession, msg, prototype, speakTime, ent.Comp.SlideTime, ent.Comp.WaddleInterval);
+ }
+
+ args.Handled = true;
+ }
+}
cmd-tippy-desc = Broadcast a message as Tippy the clown.
-cmd-tippy-help = tippy <user | all> <message> [entity prototype] [speak time] [slide time] [waddle interval]
+cmd-tippy-help = tippy <user | all> <message> [entity prototype | null] [speak time] [slide time] [waddle interval]
cmd-tippy-auto-1 = <user | all>
cmd-tippy-auto-2 = message
cmd-tippy-auto-3 = entity prototype
Commands:
- listplayers
-- Flags: FUN
- Commands:
- - tippy
- - tip
-
- Flags: SERVER
Commands:
- delete