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
2 changes: 2 additions & 0 deletions RustPlusBot.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
<Project Path="src/RustPlusBot.Features.Pairing/RustPlusBot.Features.Pairing.csproj" />
<Project Path="src/RustPlusBot.Features.Chat/RustPlusBot.Features.Chat.csproj" />
<Project Path="src/RustPlusBot.Features.Events/RustPlusBot.Features.Events.csproj" />
<Project Path="src/RustPlusBot.Features.Players/RustPlusBot.Features.Players.csproj" />
<Project Path="src/RustPlusBot.Features.Workspace/RustPlusBot.Features.Workspace.csproj" />
<Project Path="src/RustPlusBot.Persistence/RustPlusBot.Persistence.csproj" />
</Folder>
Expand All @@ -19,6 +20,7 @@
<Project Path="tests/RustPlusBot.Features.Connections.Tests/RustPlusBot.Features.Connections.Tests.csproj" />
<Project Path="tests/RustPlusBot.Features.Map.Tests/RustPlusBot.Features.Map.Tests.csproj" />
<Project Path="tests/RustPlusBot.Features.Pairing.Tests/RustPlusBot.Features.Pairing.Tests.csproj" />
<Project Path="tests/RustPlusBot.Features.Players.Tests/RustPlusBot.Features.Players.Tests.csproj" />
<Project Path="tests/RustPlusBot.Features.Workspace.Tests/RustPlusBot.Features.Workspace.Tests.csproj" />
<Project Path="tests/RustPlusBot.Features.Chat.Tests/RustPlusBot.Features.Chat.Tests.csproj" />
<Project Path="tests/RustPlusBot.Features.Events.Tests/RustPlusBot.Features.Events.Tests.csproj" />
Expand Down
6 changes: 5 additions & 1 deletion src/RustPlusBot.Abstractions/Connections/TeamInfoSnapshot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ namespace RustPlusBot.Features.Connections.Listening;
/// <summary>A point-in-time view of a team, decoupled from RustPlusApi types.</summary>
/// <param name="LeaderSteamId">Steam64 id of the current team leader.</param>
/// <param name="Members">Status snapshots for all team members.</param>
public sealed record TeamInfoSnapshot(ulong LeaderSteamId, IReadOnlyList<TeamMemberSnapshot> Members);
/// <param name="DeathNote">The leader's death-note map coordinate, if one is set; null otherwise.</param>
public sealed record TeamInfoSnapshot(
ulong LeaderSteamId,
IReadOnlyList<TeamMemberSnapshot> Members,
(float X, float Y)? DeathNote = null);

/// <summary>A point-in-time view of one team member.</summary>
/// <param name="SteamId">Steam64 id of the member.</param>
Expand Down
14 changes: 14 additions & 0 deletions src/RustPlusBot.Abstractions/Events/PlayerStateChangedEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using RustPlusBot.Features.Connections.Listening;

namespace RustPlusBot.Abstractions.Events;

/// <summary>Published when a team-info poll detects presence transitions since the previous poll.</summary>
/// <param name="GuildId">The owning guild snowflake.</param>
/// <param name="ServerId">The target server id.</param>
/// <param name="Dimensions">Map dimensions for grid rendering, or null if unavailable.</param>
/// <param name="Transitions">The transitions detected this poll (never empty when published).</param>
public sealed record PlayerStateChangedEvent(
ulong GuildId,
Guid ServerId,
MapDimensions? Dimensions,
IReadOnlyList<PlayerTransition> Transitions);
12 changes: 12 additions & 0 deletions src/RustPlusBot.Abstractions/Events/PlayerTransition.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace RustPlusBot.Abstractions.Events;

/// <summary>One team-member presence transition, with a pre-resolved optional map location.</summary>
/// <param name="Kind">The transition kind.</param>
/// <param name="SteamId">Steam64 id of the member.</param>
/// <param name="Name">In-game display name (empty when the game reports none).</param>
/// <param name="Location">Resolved map coordinate to show, or null when no location applies.</param>
public sealed record PlayerTransition(
PlayerTransitionKind Kind,
ulong SteamId,
string Name,
(float X, float Y)? Location);
23 changes: 23 additions & 0 deletions src/RustPlusBot.Abstractions/Events/PlayerTransitionKind.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace RustPlusBot.Abstractions.Events;

/// <summary>The kind of team-member presence transition detected between two team snapshots.</summary>
public enum PlayerTransitionKind
{
/// <summary>Member came online.</summary>
Connect = 0,

/// <summary>Member went offline.</summary>
Disconnect = 1,

/// <summary>Member died.</summary>
Death = 2,

/// <summary>Member spawned/respawned.</summary>
Respawn = 3,

/// <summary>Member crossed the AFK stillness threshold.</summary>
BecameAfk = 4,

/// <summary>Member moved again after being AFK.</summary>
ReturnedFromAfk = 5,
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public static IServiceCollection AddCommands(this IServiceCollection services)
services.AddScoped<ICommandHandler, TeamCommandHandler>();
services.AddScoped<ICommandHandler, SteamIdCommandHandler>();
services.AddScoped<ICommandHandler, AliveCommandHandler>();
services.AddScoped<ICommandHandler, AfkCommandHandler>();
services.AddScoped<ICommandHandler, ProxCommandHandler>();
services.AddScoped<ICommandHandler, CargoCommandHandler>();
services.AddScoped<ICommandHandler, HeliCommandHandler>();
Expand Down
38 changes: 38 additions & 0 deletions src/RustPlusBot.Features.Commands/Handlers/AfkCommandHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using RustPlusBot.Features.Commands.Dispatching;
using RustPlusBot.Features.Commands.Formatting;
using RustPlusBot.Features.Commands.Localization;
using RustPlusBot.Features.Connections.Listening;

namespace RustPlusBot.Features.Commands.Handlers;

/// <summary>!afk — lists team members who have been still (online + alive) past the AFK threshold.</summary>
/// <param name="afk">The live AFK state.</param>
/// <param name="localizer">The reply localizer.</param>
internal sealed class AfkCommandHandler(IAfkState afk, ICommandLocalizer localizer) : ICommandHandler
{
/// <inheritdoc />
public string Name => "afk";

/// <inheritdoc />
public async Task<string?> ExecuteAsync(CommandContext context, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(context);
var members = await afk.GetAfkMembersAsync(context.GuildId, context.ServerId, cancellationToken)
.ConfigureAwait(false);
if (members is null)
{
return localizer.Get("command.notconnected", context.Culture);
}

if (members.Count == 0)
{
return localizer.Get("command.afk.none", context.Culture);
}

var parts = members
.OrderByDescending(m => m.StillFor)
.Select(m => localizer.Get(
"command.afk.member", context.Culture, m.Name, DurationFormat.Compact(m.StillFor)));
return localizer.Get("command.afk.ok", context.Culture, string.Join(", ", parts));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ internal static class CommandHelpCatalog
new("team", CommandGroup.TeamIntel, "help.team"),
new("steamid", CommandGroup.TeamIntel, "help.steamid"),
new("alive", CommandGroup.TeamIntel, "help.alive"),
new("afk", CommandGroup.TeamIntel, "help.afk"),
new("prox", CommandGroup.TeamIntel, "help.prox"),
new("uptime", CommandGroup.Bot, "help.uptime"),
];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ internal sealed class CommandLocalizationCatalog
["command.team.none"] = "No team members.",
["command.team.nomatch"] = "No teammate matches '{0}'.",
["command.steamid.ok"] = "{0}",
["command.afk.ok"] = "AFK: {0}",
["command.afk.none"] = "Nobody is AFK.",
["command.afk.member"] = "{0} ({1})",
["command.alive.ok"] = "Alive: {0}",
["command.alive.dead"] = "{0} dead",
["command.alive.member"] = "{0} {1}",
Expand Down Expand Up @@ -77,6 +80,7 @@ internal sealed class CommandLocalizationCatalog
["help.team"] = "List all team members.",
["help.steamid"] = "Show teammates' Steam IDs.",
["help.alive"] = "Show how long each teammate has survived.",
["help.afk"] = "List teammates who are AFK.",
["help.prox"] = "Show distance to each teammate.",
["help.uptime"] = "Show how long the bot has been running.",
["help.slash.help"] = "Show this help message.",
Expand Down Expand Up @@ -111,6 +115,9 @@ internal sealed class CommandLocalizationCatalog
["command.team.none"] = "Aucun membre d'équipe.",
["command.team.nomatch"] = "Aucun coéquipier ne correspond à « {0} ».",
["command.steamid.ok"] = "{0}",
["command.afk.ok"] = "AFK : {0}",
["command.afk.none"] = "Personne n'est AFK.",
["command.afk.member"] = "{0} ({1})",
["command.alive.ok"] = "En vie : {0}",
["command.alive.dead"] = "{0} mort",
["command.alive.member"] = "{0} {1}",
Expand Down Expand Up @@ -158,6 +165,7 @@ internal sealed class CommandLocalizationCatalog
["help.team"] = "Lister tous les membres de l'équipe.",
["help.steamid"] = "Afficher les identifiants Steam des coéquipiers.",
["help.alive"] = "Afficher depuis combien de temps chaque coéquipier survit.",
["help.afk"] = "Lister les coéquipiers qui sont AFK.",
["help.prox"] = "Afficher la distance à chaque coéquipier.",
["help.uptime"] = "Afficher depuis combien de temps le bot fonctionne.",
["help.slash.help"] = "Afficher ce message d'aide.",
Expand Down
6 changes: 6 additions & 0 deletions src/RustPlusBot.Features.Connections/ConnectionOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,10 @@ public sealed class ConnectionOptions

/// <summary>How often the rig-timer tick advances rig phases and emits timed boundary events. Default 30s.</summary>
public TimeSpan RigTickInterval { get; set; } = TimeSpan.FromSeconds(30);

/// <summary>How long a member must be still (and online + alive) before being flagged AFK. Default 5m.</summary>
public TimeSpan AfkThreshold { get; set; } = TimeSpan.FromMinutes(5);

/// <summary>Movement tolerance (world units) below which a member is considered still. Default 1.</summary>
public float AfkEpsilon { get; set; } = 1f;
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public static IServiceCollection AddConnections(this IServiceCollection services
services.AddSingleton<IConnectionSupervisor>(sp => sp.GetRequiredService<ConnectionSupervisor>());
services.AddSingleton<ITeamChatSender>(sp => sp.GetRequiredService<ConnectionSupervisor>());
services.AddSingleton<IRustServerQuery>(sp => sp.GetRequiredService<ConnectionSupervisor>());
services.AddSingleton<IAfkState>(sp => sp.GetRequiredService<ConnectionSupervisor>());
services.AddScoped<IServerRemovalService, ServerRemovalService>();

// Contribute this assembly's interaction modules to the Discord layer.
Expand Down
20 changes: 20 additions & 0 deletions src/RustPlusBot.Features.Connections/Listening/IAfkState.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
namespace RustPlusBot.Features.Connections.Listening;

/// <summary>One currently-AFK team member.</summary>
/// <param name="SteamId">Steam64 id.</param>
/// <param name="Name">In-game display name.</param>
/// <param name="StillFor">How long the member has been continuously still.</param>
public sealed record AfkMember(ulong SteamId, string Name, TimeSpan StillFor);

/// <summary>Reads the live AFK state computed by the connection poll loop.</summary>
public interface IAfkState
{
/// <summary>Gets the currently-AFK members, or null when there is no live socket.</summary>
/// <param name="guildId">The owning guild snowflake.</param>
/// <param name="serverId">The target server id.</param>
/// <param name="cancellationToken">A cancellation token.</param>
/// <returns>The currently-AFK members, or null when there is no live socket.</returns>
Task<IReadOnlyList<AfkMember>?> GetAfkMembersAsync(ulong guildId,
Guid serverId,
CancellationToken cancellationToken);
}
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,10 @@ public async Task<HeartbeatResult> GetInfoAsync(TimeSpan timeout, CancellationTo
new DateTimeOffset(DateTime.SpecifyKind(m.LastSpawnTime, DateTimeKind.Utc)),
new DateTimeOffset(DateTime.SpecifyKind(m.LastDeathTime, DateTimeKind.Utc))))
.ToList();
return new TeamInfoSnapshot(response.Data.LeaderSteamId, members);
var deathNote = response.Data.DeathNote is { } dn
? ((float X, float Y)?)(dn.X, dn.Y)
: null;
return new TeamInfoSnapshot(response.Data.LeaderSteamId, members, deathNote);
}
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
{
Expand Down
Loading
Loading