Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
200 changes: 200 additions & 0 deletions apps/utility-sw/src/Events/PracticeAfkWatch.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
using System.Collections.Concurrent;
using FiveStack.Utilities;
using Microsoft.Extensions.Logging;
using SwiftlyS2.Shared.Events;
using SwiftlyS2.Shared.Misc;
using SwiftlyS2.Shared.Natives;
using SwiftlyS2.Shared.Players;
using SwiftlyS2.Shared.ProtobufDefinitions;
using SwiftlyS2.Shared.SchemaDefinitions;
using static SwiftlyS2.Shared.Helper;

namespace UtilityPractice;

public partial class UtilityPracticePlugin
{
private readonly PracticeAfk _afk = new();

// Player slots with any input since the last sweep. Taps are shorter than
// the one-second sample, so the input paths mark them as they happen.
private readonly ConcurrentDictionary<int, byte> _afkStirred = new();

private Guid? _afkChatHook;
private EventDelegates.OnClientVoice? _voiceHandler;

private static int AfkNow => (int)(Environment.TickCount64 / 1000);

private void Stir(int playerId)
{
_afkStirred[playerId] = 0;
}

private void WireAfk()
{
_afkChatHook = Core.Command.HookClientChat(
(playerId, _, _) =>
{
Stir(playerId);

return HookResult.Continue;
}
);

_voiceHandler = @event => Stir(@event.PlayerId);
Core.Event.OnClientVoice += _voiceHandler;
}

private void UnwireAfk()
{
if (_afkChatHook is { } hook)
{
Core.Command.UnhookClientChat(hook);
_afkChatHook = null;
}

if (_voiceHandler != null)
{
Core.Event.OnClientVoice -= _voiceHandler;
_voiceHandler = null;
}

_afkStirred.Clear();
_afk.Reset();
}

private void AfkWatch()
{
if (_config.RenderMode)
{
return;
}

var samples = new List<PracticeAfk.Sample>();
var players = new Dictionary<ulong, IPlayer>();

foreach (IPlayer player in Core.PlayerManager.GetAllPlayers())
{
if (player == null || !player.IsValid || player.IsFakeClient)
{
continue;
}

players[player.SteamID] = player;
samples.Add(AfkSample(player));
}

_afkStirred.Clear();

int minutes = _session.Current?.afk_kick_minutes ?? 0;

PracticeAfk.Dispatch(
_afk.Sweep(samples, AfkNow, minutes),
due =>
{
if (!players.TryGetValue(due.SteamId, out IPlayer? player))
{
return;
}

if (due.Step == PracticeAfk.Step.Kick)
{
AfkKick(player, due.IdleSeconds);

return;
}

AfkWarn(player, PracticeAfk.Warning(due.Step, due.SecondsLeft));
},
(due, error) =>
_logger.LogWarning(error, "unable to act on {step} for {steam}", due.Step, due.SteamId)
);
}

// A dead or spectating player's own pawn is frozen, held buttons and all,
// so they are judged by the observer pawn they look through and by what
// they press, say or type. A player who cannot be read is taken as present
// rather than kicked on a fault.
private PracticeAfk.Sample AfkSample(IPlayer player)
{
try
{
bool inGame = player.Controller.Connected == PlayerConnectedState.Connected;
bool alive = player.IsAlive;
bool stirred = _afkStirred.ContainsKey(player.PlayerID);
ulong buttons = alive ? (ulong)player.PressedButtons : 0;

if (!alive)
{
CBasePlayerPawn? observer = player.Pawn;

if (observer == null || !observer.IsValid)
{
return new PracticeAfk.Sample(player.SteamID, inGame, Alive: alive, stirred, 0, null, null);
}

bool roaming =
observer.ObserverServices?.ObserverMode == (byte)ObserverMode_t.OBS_MODE_ROAMING;
QAngle view = observer.V_angle;
Vector seat = observer.AbsOrigin ?? new Vector(0, 0, 0);

return new PracticeAfk.Sample(
player.SteamID,
inGame,
Alive: alive,
stirred,
0,
(view.Y, view.X),
(seat.X, seat.Y, seat.Z),
Roaming: roaming
);
}

CCSPlayerPawn? pawn = player.PlayerPawn;

if (pawn == null || !pawn.IsValid)
{
return new PracticeAfk.Sample(player.SteamID, inGame, Alive: alive, stirred, buttons, null, null);
}

QAngle eyes = pawn.EyeAngles;
Vector at = pawn.AbsOrigin ?? new Vector(0, 0, 0);

return new PracticeAfk.Sample(
player.SteamID,
inGame,
Alive: alive,
stirred,
buttons,
(eyes.Y, eyes.X),
(at.X, at.Y, at.Z)
);
}
catch
{
return new PracticeAfk.Sample(player.SteamID, false, false, true, 0, null, null);
}
}

private void AfkWarn(IPlayer player, string text)
{
Tell(player.SteamID, $" {ChatColors.Red}{text}");

if (UseHud(player.SteamID) && _hud!.Available(HudSlots.Bar))
{
Toast(player, text);
}
}

private void AfkKick(IPlayer player, int idleSeconds)
{
ulong steamId = player.SteamID;

_afk.Forget(steamId);
_logger.LogInformation("kicking {steam}: idle for {seconds} seconds", steamId, idleSeconds);

player.Kick(
PracticeAfk.KickReason(idleSeconds),
ENetworkDisconnectionReason.NETWORK_DISCONNECT_KICKED_IDLE
);
}
}
2 changes: 2 additions & 0 deletions apps/utility-sw/src/Hud/PracticeHud.cs
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,8 @@ bool onAngle

private void OnHudClicked(IPlayer player, string layout, string button)
{
Stir(player.PlayerID);

if (layout == HudSlots.NadeEdit)
{
OnEditClicked(player, button);
Expand Down
2 changes: 2 additions & 0 deletions apps/utility-sw/src/Hud/PracticeKeys.cs
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,8 @@ private void UnwireKeys()

private void OnKey(IOnClientKeyStateChangedEvent @event)
{
Stir(@event.PlayerId);

KeyKind key = @event.Key;

if (key is not (KeyKind.Tab or KeyKind.R or KeyKind.Ctrl or KeyKind.Shift or KeyKind.F))
Expand Down
1 change: 1 addition & 0 deletions apps/utility-sw/src/UtilityPractice.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
<Compile Include="$(MSBuildThisFileDirectory)../../../shared/dotnet/FiveStack.Utilities/PracticeDrillRun.cs" LinkBase="Shared" />
<Compile Include="$(MSBuildThisFileDirectory)../../../shared/dotnet/FiveStack.Utilities/PracticeThrowTiming.cs" LinkBase="Shared" />
<Compile Include="$(MSBuildThisFileDirectory)../../../shared/dotnet/FiveStack.Utilities/PracticeMapChangeUtility.cs" LinkBase="Shared" />
<Compile Include="$(MSBuildThisFileDirectory)../../../shared/dotnet/FiveStack.Utilities/PracticeAfk.cs" LinkBase="Shared" />
<Compile Include="$(MSBuildThisFileDirectory)../../../shared/dotnet/FiveStack.Utilities/PracticeLibraryLoadUtility.cs" LinkBase="Shared" />
<Compile Include="$(MSBuildThisFileDirectory)../../../shared/dotnet/FiveStack.Utilities/HttpClientProvider.cs" LinkBase="Shared" />
<Compile Include="$(MSBuildThisFileDirectory)../../../shared/dotnet/FiveStack.Utilities/SteamIdUtility.cs" LinkBase="Shared" />
Expand Down
10 changes: 10 additions & 0 deletions apps/utility-sw/src/UtilityPracticePlugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ public override void Load(bool hotReload)
_tickHandler = OnGameTick;
Core.Event.OnTick += _tickHandler;
WireKeys();
WireAfk();

if (_hud != null)
{
Expand Down Expand Up @@ -480,6 +481,7 @@ private void TearDown()

_prompt?.Stop();
UnwireKeys();
UnwireAfk();

if (_tickHandler != null)
{
Expand Down Expand Up @@ -1040,6 +1042,11 @@ private void UseWatch()
continue;
}

if (buttons != 0 && player.IsAlive)
{
Stir(player.PlayerID);
}

bool down = (buttons & InUse) != 0;

// The edge, not the state: holding use must not teleport once a
Expand Down Expand Up @@ -1932,6 +1939,7 @@ private void OnSecond()
DrainLibraryLoads();
DrainPendingMapLoad();
KeepWorldsCurrent();
AfkWatch();
_ = _api.Second();
}

Expand Down Expand Up @@ -2144,6 +2152,7 @@ private void OnPlayerGone(ulong steamId)
SendHeldSaves(steamId);
_drill.Forget(steamId);
_system.Forget(steamId);
_afk.Forget(steamId);

_replay.ForgetPlayer(steamId);
_shown.Remove(steamId);
Expand Down Expand Up @@ -2223,6 +2232,7 @@ private void OnMapLoad(string mapName)
// its thrower's pawn went with the map.
_director.Reset(null);
_hud?.Reset();
_afk.Reset();
_menus.Clear();
_shown.Clear();
_standingIn.Clear();
Expand Down
1 change: 1 addition & 0 deletions apps/utility-sw/test/FiveStack.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
<Compile Include="$(MSBuildThisFileDirectory)../../../shared/dotnet/FiveStack.Utilities/PracticeDrillRun.cs" LinkBase="Shared" />
<Compile Include="$(MSBuildThisFileDirectory)../../../shared/dotnet/FiveStack.Utilities/PracticeThrowTiming.cs" LinkBase="Shared" />
<Compile Include="$(MSBuildThisFileDirectory)../../../shared/dotnet/FiveStack.Utilities/PracticeMapChangeUtility.cs" LinkBase="Shared" />
<Compile Include="$(MSBuildThisFileDirectory)../../../shared/dotnet/FiveStack.Utilities/PracticeAfk.cs" LinkBase="Shared" />
<Compile Include="$(MSBuildThisFileDirectory)../../../shared/dotnet/FiveStack.Utilities/PracticeLibraryLoadUtility.cs" LinkBase="Shared" />
<Compile Include="$(MSBuildThisFileDirectory)../../../shared/dotnet/FiveStack.Utilities/HudSlots.cs" LinkBase="Shared" />
<Compile Include="$(MSBuildThisFileDirectory)../../../shared/dotnet/FiveStack.Utilities/HudLayoutContract.cs" LinkBase="Shared" />
Expand Down
Loading
Loading