Skip to content

Commit 5d7d324

Browse files
HandyS11claude
andcommitted
feat(players): publish + relay PlayerStateChangedEvent from poll loop
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 3c4fe4c commit 5d7d324

6 files changed

Lines changed: 151 additions & 0 deletions

File tree

src/RustPlusBot.Features.Connections/Supervisor/ConnectionSupervisor.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,7 @@ private async Task PollMarkersAsync(
449449
{
450450
IReadOnlyList<MapMarkerSnapshot>? previous = null;
451451
var rigsInRadius = new HashSet<RigKind>();
452+
var tracker = new TeamStateTracker();
452453
while (!ct.IsCancellationRequested)
453454
{
454455
var anyCh47 = false;
@@ -475,6 +476,15 @@ await eventBus.PublishAsync(
475476
}
476477

477478
await DetectRigActivationsAsync(key, current, rigs, dims, rigsInRadius, ct).ConfigureAwait(false);
479+
480+
var team = await connection.GetTeamInfoAsync(_options.HeartbeatTimeout, ct).ConfigureAwait(false);
481+
var transitions = tracker.Diff(team);
482+
if (transitions.Count > 0)
483+
{
484+
await eventBus.PublishAsync(
485+
new PlayerStateChangedEvent(key.Guild, key.Server, dims, transitions), ct)
486+
.ConfigureAwait(false);
487+
}
478488
}
479489
catch (OperationCanceledException)
480490
{
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
using Microsoft.Extensions.Hosting;
2+
using Microsoft.Extensions.Logging;
3+
using RustPlusBot.Abstractions.Events;
4+
using RustPlusBot.Features.Players.Relaying;
5+
6+
namespace RustPlusBot.Features.Players.Hosting;
7+
8+
/// <summary>Consumes <see cref="PlayerStateChangedEvent"/> and relays each to #events + in-game chat.</summary>
9+
/// <param name="eventBus">The in-process event bus.</param>
10+
/// <param name="relay">Relays player transitions.</param>
11+
/// <param name="logger">The logger.</param>
12+
internal sealed partial class PlayersHostedService(
13+
IEventBus eventBus,
14+
PlayerEventRelay relay,
15+
ILogger<PlayersHostedService> logger) : IHostedService, IDisposable
16+
{
17+
private readonly CancellationTokenSource _cts = new();
18+
private Task? _loop;
19+
20+
/// <inheritdoc />
21+
public void Dispose() => _cts.Dispose();
22+
23+
/// <inheritdoc />
24+
public Task StartAsync(CancellationToken cancellationToken)
25+
{
26+
_loop = Task.Run(() => ConsumeAsync(_cts.Token), CancellationToken.None);
27+
return Task.CompletedTask;
28+
}
29+
30+
/// <inheritdoc />
31+
public async Task StopAsync(CancellationToken cancellationToken)
32+
{
33+
await _cts.CancelAsync().ConfigureAwait(false);
34+
if (_loop is not null)
35+
{
36+
try
37+
{
38+
#pragma warning disable VSTHRD003 // Our own loop task, joined on stop.
39+
await _loop.ConfigureAwait(false);
40+
#pragma warning restore VSTHRD003
41+
}
42+
catch (OperationCanceledException)
43+
{
44+
// Expected on shutdown.
45+
}
46+
}
47+
}
48+
49+
private async Task ConsumeAsync(CancellationToken cancellationToken)
50+
{
51+
try
52+
{
53+
await foreach (var evt in eventBus.SubscribeAsync<PlayerStateChangedEvent>(cancellationToken)
54+
.ConfigureAwait(false))
55+
{
56+
await relay.RelayAsync(evt, cancellationToken).ConfigureAwait(false);
57+
}
58+
}
59+
catch (OperationCanceledException)
60+
{
61+
// Shutting down.
62+
}
63+
#pragma warning disable CA1031 // Broad catch: a faulting consumer must not crash the host.
64+
catch (Exception ex)
65+
#pragma warning restore CA1031
66+
{
67+
LogRelayLoopFaulted(logger, ex);
68+
}
69+
}
70+
71+
[LoggerMessage(Level = LogLevel.Error, Message = "Player relay loop faulted.")]
72+
private static partial void LogRelayLoopFaulted(ILogger logger, Exception exception);
73+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
using Microsoft.Extensions.DependencyInjection;
2+
using RustPlusBot.Features.Players.Hosting;
3+
using RustPlusBot.Features.Players.Posting;
4+
using RustPlusBot.Features.Players.Relaying;
5+
using RustPlusBot.Features.Players.Rendering;
6+
7+
namespace RustPlusBot.Features.Players;
8+
9+
/// <summary>DI registration for the team-presence-events feature.</summary>
10+
public static class PlayerEventServiceCollectionExtensions
11+
{
12+
/// <summary>Registers the localizer, renderer, poster, relay, and hosted service.</summary>
13+
/// <param name="services">The service collection to add to.</param>
14+
/// <returns>The same service collection, for chaining.</returns>
15+
public static IServiceCollection AddPlayers(this IServiceCollection services)
16+
{
17+
ArgumentNullException.ThrowIfNull(services);
18+
19+
services.AddSingleton(PlayerLocalizationCatalog.Default);
20+
services.AddSingleton<IPlayerLocalizer, PlayerLocalizer>();
21+
services.AddSingleton<PlayerEventRenderer>();
22+
services.AddSingleton<IPlayerChannelPoster, DiscordPlayerChannelPoster>();
23+
services.AddSingleton<PlayerEventRelay>();
24+
services.AddHostedService<PlayersHostedService>();
25+
26+
return services;
27+
}
28+
}

src/RustPlusBot.Host/Program.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
using RustPlusBot.Features.Connections;
1212
using RustPlusBot.Features.Events;
1313
using RustPlusBot.Features.Map;
14+
using RustPlusBot.Features.Players;
1415
using RustPlusBot.Features.Pairing;
1516
using RustPlusBot.Features.Workspace;
1617
using RustPlusBot.Host.Credentials;
@@ -68,6 +69,7 @@
6869
.ValidateOnStart();
6970
builder.Services.AddCommands();
7071
builder.Services.AddEvents();
72+
builder.Services.AddPlayers();
7173
builder.Services.AddOptions<MapOptions>()
7274
.Bind(builder.Configuration.GetSection("Map"))
7375
.Validate(static o => o.MapRefreshInterval > TimeSpan.Zero, "Map:MapRefreshInterval must be positive.")

src/RustPlusBot.Host/RustPlusBot.Host.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,6 @@
2525
<ProjectReference Include="..\RustPlusBot.Features.Commands\RustPlusBot.Features.Commands.csproj" />
2626
<ProjectReference Include="..\RustPlusBot.Features.Events\RustPlusBot.Features.Events.csproj" />
2727
<ProjectReference Include="..\RustPlusBot.Features.Map\RustPlusBot.Features.Map.csproj" />
28+
<ProjectReference Include="..\RustPlusBot.Features.Players\RustPlusBot.Features.Players.csproj" />
2829
</ItemGroup>
2930
</Project>
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
using Discord.WebSocket;
2+
using Microsoft.Extensions.DependencyInjection;
3+
using Microsoft.Extensions.Hosting;
4+
using NSubstitute;
5+
using RustPlusBot.Abstractions.Events;
6+
using RustPlusBot.Features.Connections.Listening;
7+
using RustPlusBot.Features.Players;
8+
using RustPlusBot.Features.Players.Hosting;
9+
using RustPlusBot.Features.Players.Relaying;
10+
using RustPlusBot.Features.Workspace.Locating;
11+
using RustPlusBot.Persistence.Workspace;
12+
13+
namespace RustPlusBot.Features.Players.Tests;
14+
15+
public sealed class PlayerEventRegistrationTests
16+
{
17+
[Fact]
18+
public void Services_resolve()
19+
{
20+
using var provider = BuildProvider();
21+
Assert.Contains(provider.GetServices<IHostedService>(), h => h is PlayersHostedService);
22+
Assert.NotNull(provider.GetRequiredService<PlayerEventRelay>());
23+
}
24+
25+
private static ServiceProvider BuildProvider()
26+
{
27+
var services = new ServiceCollection();
28+
services.AddLogging();
29+
services.AddSingleton<IEventBus, InMemoryEventBus>();
30+
services.AddSingleton(new DiscordSocketClient());
31+
services.AddSingleton<IEventChannelLocator>(Substitute.For<IEventChannelLocator>());
32+
services.AddScoped<IWorkspaceStore>(_ => Substitute.For<IWorkspaceStore>());
33+
services.AddSingleton<ITeamChatSender>(Substitute.For<ITeamChatSender>());
34+
services.AddPlayers();
35+
return services.BuildServiceProvider(validateScopes: true);
36+
}
37+
}

0 commit comments

Comments
 (0)