From 7345985410451acbd4b1dd08b924ee3d28a2ce19 Mon Sep 17 00:00:00 2001 From: Pieter-Jan Briers Date: Thu, 22 Jul 2021 23:19:41 +0200 Subject: [PATCH] Add AFK tracking system. Allows us to see which players have not done any input and can be considered AFK. --- Content.Server/Afk/AfkManager.cs | 103 +++++++++++++++++++++++++ Content.Server/Afk/IsAfkCommand.cs | 36 +++++++++ Content.Server/Entry/EntryPoint.cs | 2 + Content.Server/IoC/ServerContentIoC.cs | 2 + Content.Shared/CCVar/CCVars.cs | 10 +++ 5 files changed, 153 insertions(+) create mode 100644 Content.Server/Afk/AfkManager.cs create mode 100644 Content.Server/Afk/IsAfkCommand.cs diff --git a/Content.Server/Afk/AfkManager.cs b/Content.Server/Afk/AfkManager.cs new file mode 100644 index 0000000000..ae93e81254 --- /dev/null +++ b/Content.Server/Afk/AfkManager.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using Content.Shared.CCVar; +using JetBrains.Annotations; +using Robust.Server.Player; +using Robust.Shared.Configuration; +using Robust.Shared.Console; +using Robust.Shared.Enums; +using Robust.Shared.GameObjects; +using Robust.Shared.Input; +using Robust.Shared.IoC; +using Robust.Shared.Timing; + +namespace Content.Server.Afk +{ + /// + /// Tracks AFK (away from keyboard) status for players. + /// + /// + public interface IAfkManager + { + /// + /// Check whether this player is currently AFK. + /// + /// The player to check. + /// True if the player is AFK, false otherwise. + bool IsAfk(IPlayerSession player); + + /// + /// Resets AFK status for the player as if they just did an action and are definitely not AFK. + /// + /// The player to set AFK status for. + void PlayerDidAction(IPlayerSession player); + + void Initialize(); + } + + [UsedImplicitly] + public class AfkManager : IAfkManager, IEntityEventSubscriber + { + [Dependency] private readonly IEntityManager _entityManager = default!; + [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly IGameTiming _gameTiming = default!; + [Dependency] private readonly IConfigurationManager _cfg = default!; + [Dependency] private readonly IConsoleHost _consoleHost = default!; + + private readonly Dictionary _lastActionTimes = new(); + + public void Initialize() + { + // Connecting, console commands and input commands all reset AFK status. + + _playerManager.PlayerStatusChanged += PlayerStatusChanged; + _consoleHost.AnyCommandExecuted += ConsoleHostOnAnyCommandExecuted; + + _entityManager.EventBus.SubscribeSessionEvent( + EventSource.Network, + this, + HandleInputCmd); + } + + public void PlayerDidAction(IPlayerSession player) + { + if (player.Status == SessionStatus.Disconnected) + // Make sure we don't re-add to the dictionary if the player is disconnected now. + return; + + _lastActionTimes[player] = _gameTiming.RealTime; + } + + public bool IsAfk(IPlayerSession player) + { + if (!_lastActionTimes.TryGetValue(player, out var time)) + // Some weird edge case like disconnected clients. Just say true I guess. + return true; + + var timeOut = TimeSpan.FromSeconds(_cfg.GetCVar(CCVars.AfkTime)); + return _gameTiming.RealTime - time > timeOut; + } + + private void PlayerStatusChanged(object? sender, SessionStatusEventArgs e) + { + if (e.NewStatus == SessionStatus.Disconnected) + { + _lastActionTimes.Remove(e.Session); + return; + } + + PlayerDidAction(e.Session); + } + + private void ConsoleHostOnAnyCommandExecuted(IConsoleShell shell, string commandname, string argstr, string[] args) + { + if (shell.Player is IPlayerSession player) + PlayerDidAction(player); + } + + private void HandleInputCmd(FullInputCmdMessage msg, EntitySessionEventArgs args) + { + PlayerDidAction((IPlayerSession) args.SenderSession); + } + } +} diff --git a/Content.Server/Afk/IsAfkCommand.cs b/Content.Server/Afk/IsAfkCommand.cs new file mode 100644 index 0000000000..b166851b50 --- /dev/null +++ b/Content.Server/Afk/IsAfkCommand.cs @@ -0,0 +1,36 @@ +using Content.Server.Administration; +using Content.Shared.Administration; +using Robust.Server.Player; +using Robust.Shared.Console; +using Robust.Shared.IoC; + +namespace Content.Server.Afk +{ + [AdminCommand(AdminFlags.Admin)] + public sealed class IsAfkCommand : IConsoleCommand + { + public string Command => "isafk"; + public string Description => "Checks if a specified player is AFK"; + public string Help => "Usage: isafk "; + + public void Execute(IConsoleShell shell, string argStr, string[] args) + { + var playerManager = IoCManager.Resolve(); + var afkManager = IoCManager.Resolve(); + + if (args.Length == 0) + { + shell.WriteError("Need one argument"); + return; + } + + if (!playerManager.TryGetSessionByUsername(args[0], out var player)) + { + shell.WriteError("Unable to find that player"); + return; + } + + shell.WriteLine(afkManager.IsAfk(player) ? "They are indeed AFK" : "They are not AFK"); + } + } +} diff --git a/Content.Server/Entry/EntryPoint.cs b/Content.Server/Entry/EntryPoint.cs index 7f5e56b98e..621801dc93 100644 --- a/Content.Server/Entry/EntryPoint.cs +++ b/Content.Server/Entry/EntryPoint.cs @@ -1,4 +1,5 @@ using Content.Server.Administration.Managers; +using Content.Server.Afk; using Content.Server.AI.Utility; using Content.Server.AI.Utility.Considerations; using Content.Server.AI.WorldState; @@ -92,6 +93,7 @@ namespace Content.Server.Entry IoCManager.Resolve().Initialize(); IoCManager.Resolve().Initialize(); IoCManager.Resolve().Initialize(); + IoCManager.Resolve().Initialize(); _euiManager.Initialize(); IoCManager.Resolve().GetEntitySystem().PostInitialize(); diff --git a/Content.Server/IoC/ServerContentIoC.cs b/Content.Server/IoC/ServerContentIoC.cs index e6f5d5238a..4e1fabb0bf 100644 --- a/Content.Server/IoC/ServerContentIoC.cs +++ b/Content.Server/IoC/ServerContentIoC.cs @@ -1,5 +1,6 @@ using Content.Server.Administration; using Content.Server.Administration.Managers; +using Content.Server.Afk; using Content.Server.AI.Utility; using Content.Server.AI.Utility.Considerations; using Content.Server.AI.WorldState; @@ -62,6 +63,7 @@ namespace Content.Server.IoC IoCManager.Register(); IoCManager.Register(); IoCManager.Register(); + IoCManager.Register(); } } } diff --git a/Content.Shared/CCVar/CCVars.cs b/Content.Shared/CCVar/CCVars.cs index a3ae9c1bad..e9d333244d 100644 --- a/Content.Shared/CCVar/CCVars.cs +++ b/Content.Shared/CCVar/CCVars.cs @@ -382,5 +382,15 @@ namespace Content.Shared.CCVar public static readonly CVarDef ChatMaxMessageLength = CVarDef.Create("chat.max_message_length", 1000, CVar.SERVER | CVar.REPLICATED); + + /* + * AFK + */ + + /// + /// How long a client can go without any input before being considered AFK. + /// + public static readonly CVarDef AfkTime = + CVarDef.Create("afk.time", 60f, CVar.SERVERONLY); } }