Add AFK tracking system.

Allows us to see which players have not done any input and can be considered AFK.
This commit is contained in:
Pieter-Jan Briers
2021-07-22 23:19:41 +02:00
parent ed384fbc91
commit 7345985410
5 changed files with 153 additions and 0 deletions

View File

@@ -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
{
/// <summary>
/// Tracks AFK (away from keyboard) status for players.
/// </summary>
/// <seealso cref="CCVars.AfkTime"/>
public interface IAfkManager
{
/// <summary>
/// Check whether this player is currently AFK.
/// </summary>
/// <param name="player">The player to check.</param>
/// <returns>True if the player is AFK, false otherwise.</returns>
bool IsAfk(IPlayerSession player);
/// <summary>
/// Resets AFK status for the player as if they just did an action and are definitely not AFK.
/// </summary>
/// <param name="player">The player to set AFK status for.</param>
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<IPlayerSession, TimeSpan> _lastActionTimes = new();
public void Initialize()
{
// Connecting, console commands and input commands all reset AFK status.
_playerManager.PlayerStatusChanged += PlayerStatusChanged;
_consoleHost.AnyCommandExecuted += ConsoleHostOnAnyCommandExecuted;
_entityManager.EventBus.SubscribeSessionEvent<FullInputCmdMessage>(
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);
}
}
}

View File

@@ -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 <playerName>";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
var playerManager = IoCManager.Resolve<IPlayerManager>();
var afkManager = IoCManager.Resolve<IAfkManager>();
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");
}
}
}

View File

@@ -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<IPDAUplinkManager>().Initialize();
IoCManager.Resolve<IAdminManager>().Initialize();
IoCManager.Resolve<INpcBehaviorManager>().Initialize();
IoCManager.Resolve<IAfkManager>().Initialize();
_euiManager.Initialize();
IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<GameTicker>().PostInitialize();

View File

@@ -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<IVoteManager, VoteManager>();
IoCManager.Register<INpcBehaviorManager, NpcBehaviorManager>();
IoCManager.Register<IPlayerLocator, PlayerLocator>();
IoCManager.Register<IAfkManager, AfkManager>();
}
}
}

View File

@@ -382,5 +382,15 @@ namespace Content.Shared.CCVar
public static readonly CVarDef<int> ChatMaxMessageLength =
CVarDef.Create("chat.max_message_length", 1000, CVar.SERVER | CVar.REPLICATED);
/*
* AFK
*/
/// <summary>
/// How long a client can go without any input before being considered AFK.
/// </summary>
public static readonly CVarDef<float> AfkTime =
CVarDef.Create("afk.time", 60f, CVar.SERVERONLY);
}
}