Skip to content

Commit 79f7d13

Browse files
HandyS11claude
andcommitted
refactor: bundle cohesive dependencies to satisfy S107 parameter limit
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent f33fed4 commit 79f7d13

22 files changed

Lines changed: 153 additions & 102 deletions

File tree

src/RustPlusBot.Features.Alarms/AlarmServiceCollectionExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ public static IServiceCollection AddAlarms(this IServiceCollection services)
2323
services.AddSingleton<IAlarmChannelPoster, DiscordAlarmChannelPoster>();
2424
services.AddSingleton<IAlarmRefresher, AlarmRefresher>();
2525
services.AddSingleton<AlarmPairingCoordinator>();
26+
services.AddSingleton<AlarmRelayChannels>();
2627
services.AddSingleton<AlarmStateRelay>();
2728
services.AddHostedService<AlarmsHostedService>();
2829

src/RustPlusBot.Features.Alarms/Relaying/AlarmStateRelay.cs

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,24 +13,29 @@
1313

1414
namespace RustPlusBot.Features.Alarms.Relaying;
1515

16+
/// <summary>Bundles the channel/chat collaborators injected into <see cref="AlarmStateRelay"/>.</summary>
17+
/// <param name="Locator">Resolves the #alarms channel id.</param>
18+
/// <param name="Poster">Posts/edits alarm embeds and sends @everyone pings.</param>
19+
/// <param name="TeamChatSender">Relays messages into in-game team chat.</param>
20+
internal sealed record AlarmRelayChannels(
21+
IAlarmChannelLocator Locator,
22+
IAlarmChannelPoster Poster,
23+
ITeamChatSender TeamChatSender);
24+
1625
/// <summary>
1726
/// Keeps alarm embeds in sync with live socket events: updates state and re-renders on trigger; marks
1827
/// alarms unreachable when the server goes non-Connected.
1928
/// </summary>
2029
/// <param name="scopeFactory">Opens scopes for the scoped stores.</param>
2130
/// <param name="refresher">Re-renders a single alarm embed on demand.</param>
22-
/// <param name="locator">Resolves the #alarms channel id.</param>
23-
/// <param name="poster">Posts/edits alarm embeds and sends @everyone pings.</param>
24-
/// <param name="teamChatSender">Relays messages into in-game team chat.</param>
31+
/// <param name="channels">Bundles the channel/chat collaborators.</param>
2532
/// <param name="localizer">Resolves localized alarm strings.</param>
2633
/// <param name="clock">Provides the current UTC time.</param>
2734
/// <param name="logger">The logger.</param>
2835
internal sealed partial class AlarmStateRelay(
2936
IServiceScopeFactory scopeFactory,
3037
IAlarmRefresher refresher,
31-
IAlarmChannelLocator locator,
32-
IAlarmChannelPoster poster,
33-
ITeamChatSender teamChatSender,
38+
AlarmRelayChannels channels,
3439
IAlarmLocalizer localizer,
3540
IClock clock,
3641
ILogger<AlarmStateRelay> logger)
@@ -83,10 +88,11 @@ await refresher.RefreshAsync(evt.GuildId, evt.ServerId, evt.EntityId, unreachabl
8388

8489
if (ping)
8590
{
86-
var channelId = await locator.GetChannelIdAsync(evt.GuildId, evt.ServerId, ct).ConfigureAwait(false);
91+
var channelId = await channels.Locator.GetChannelIdAsync(evt.GuildId, evt.ServerId, ct)
92+
.ConfigureAwait(false);
8793
if (channelId is { } channel)
8894
{
89-
await poster.SendEveryonePingAsync(channel, $"@everyone {name}", ct).ConfigureAwait(false);
95+
await channels.Poster.SendEveryonePingAsync(channel, $"@everyone {name}", ct).ConfigureAwait(false);
9096
}
9197
}
9298

@@ -140,7 +146,7 @@ private async Task RelayToTeamChatSafeAsync(SmartDeviceTriggeredEvent evt, strin
140146
}
141147

142148
var line = localizer.Get("alarm.triggered.teamchat", culture, name);
143-
_ = await teamChatSender.SendAsync(evt.GuildId, evt.ServerId, line, ct).ConfigureAwait(false);
149+
_ = await channels.TeamChatSender.SendAsync(evt.GuildId, evt.ServerId, line, ct).ConfigureAwait(false);
144150
}
145151
catch (OperationCanceledException)
146152
{

src/RustPlusBot.Features.Connections/ConnectionServiceCollectionExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ public static IServiceCollection AddConnections(this IServiceCollection services
1919
ArgumentNullException.ThrowIfNull(services);
2020

2121
services.AddSingleton<IRustSocketSource, RustPlusSocketSource>();
22+
services.AddSingleton<ConnectionSecurity>();
2223
services.AddSingleton<ConnectionSupervisor>();
2324
services.AddSingleton<IConnectionSupervisor>(sp => sp.GetRequiredService<ConnectionSupervisor>());
2425
services.AddSingleton<ITeamChatSender>(sp => sp.GetRequiredService<ConnectionSupervisor>());

src/RustPlusBot.Features.Connections/Listening/TeamStateTracker.cs

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,13 @@
33

44
namespace RustPlusBot.Features.Connections.Listening;
55

6+
/// <summary>Bundles the polling parameters passed to the <c>UpdateAfk</c> private method in <see cref="TeamStateTracker"/>.</summary>
7+
/// <param name="Clock">The current wall-clock time.</param>
8+
/// <param name="Threshold">How long a member must be still before being flagged AFK.</param>
9+
/// <param name="Epsilon">Movement tolerance (world units) below which a member is considered still.</param>
10+
/// <param name="DiedThisPoll">Whether the member died during this poll.</param>
11+
internal sealed record AfkPollContext(DateTimeOffset Clock, TimeSpan Threshold, float Epsilon, bool DiedThisPoll);
12+
613
/// <summary>Diffs successive team snapshots into presence transitions. One instance per connected window.</summary>
714
internal sealed class TeamStateTracker
815
{
@@ -54,7 +61,8 @@ public IReadOnlyList<PlayerTransition> Diff(
5461

5562
AddPresenceTransitions(transitions, id, was, nowMember, snapshot);
5663
var diedThisPoll = nowMember.LastDeathTimeUtc > was.LastDeathTimeUtc;
57-
UpdateAfk(transitions, id, was, nowMember, now, afkThreshold, afkEpsilon, diedThisPoll);
64+
UpdateAfk(transitions, id, was, nowMember,
65+
new AfkPollContext(now, afkThreshold, afkEpsilon, diedThisPoll));
5866
}
5967

6068
PruneDepartedMembers(current);
@@ -96,30 +104,27 @@ private void UpdateAfk(
96104
ulong id,
97105
TeamMemberSnapshot was,
98106
TeamMemberSnapshot now,
99-
DateTimeOffset clock,
100-
TimeSpan threshold,
101-
float epsilon,
102-
bool diedThisPoll)
107+
AfkPollContext context)
103108
{
104109
// A member who goes offline, is dead, or died this poll (even if a slow poll already shows them
105110
// respawned) can no longer be AFK. Clear the AFK latch SILENTLY — the disconnect/death transition
106111
// already speaks for them, and a "back" line alongside "disconnected"/"died" would contradict it —
107112
// and reset the stillness clock so AFK must be re-earned after the state change.
108-
if (!now.IsOnline || !now.IsAlive || diedThisPoll)
113+
if (!now.IsOnline || !now.IsAlive || context.DiedThisPoll)
109114
{
110115
_afk.Remove(id);
111-
_stillSince[id] = clock;
116+
_stillSince[id] = context.Clock;
112117
return;
113118
}
114119

115120
// Squared-distance check so epsilon is a true movement radius (per-axis would treat diagonal
116121
// movement of dx=dy=0.8, epsilon=1 — distance ≈ 1.13 — as still).
117122
var dx = now.X - was.X;
118123
var dy = now.Y - was.Y;
119-
var moved = (dx * dx) + (dy * dy) > epsilon * epsilon;
124+
var moved = (dx * dx) + (dy * dy) > context.Epsilon * context.Epsilon;
120125
if (moved)
121126
{
122-
_stillSince[id] = clock;
127+
_stillSince[id] = context.Clock;
123128
if (_afk.Remove(id))
124129
{
125130
transitions.Add(new PlayerTransition(PlayerTransitionKind.ReturnedFromAfk, id, now.Name, null));
@@ -128,9 +133,9 @@ private void UpdateAfk(
128133
return;
129134
}
130135

131-
var since = _stillSince.TryGetValue(id, out var s) ? s : clock;
136+
var since = _stillSince.TryGetValue(id, out var s) ? s : context.Clock;
132137
_stillSince.TryAdd(id, since);
133-
if (clock - since >= threshold && _afk.Add(id))
138+
if (context.Clock - since >= context.Threshold && _afk.Add(id))
134139
{
135140
transitions.Add(new PlayerTransition(PlayerTransitionKind.BecameAfk, id, now.Name, (now.X, now.Y)));
136141
}

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

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,23 @@
1818

1919
namespace RustPlusBot.Features.Connections.Supervisor;
2020

21+
/// <summary>Bundles the security/notification collaborators injected into <see cref="ConnectionSupervisor"/>.</summary>
22+
/// <param name="DmSender">DMs an owner when their credential is rejected.</param>
23+
/// <param name="Protector">Unprotects stored tokens before connecting.</param>
24+
internal sealed record ConnectionSecurity(IUserDmSender DmSender, ICredentialProtector Protector);
25+
2126
/// <summary>Default <see cref="IConnectionSupervisor"/>: one connect->heartbeat->failover loop per (guild, server).</summary>
2227
/// <param name="source">Creates sockets (RustPlusApi in production, a fake in tests).</param>
2328
/// <param name="scopeFactory">Opens scopes for the scoped stores.</param>
24-
/// <param name="dmSender">DMs an owner when their credential is rejected.</param>
25-
/// <param name="protector">Unprotects stored tokens before connecting.</param>
29+
/// <param name="security">Bundles the security/notification collaborators.</param>
2630
/// <param name="eventBus">Publishes ConnectionStatusChangedEvent on state changes.</param>
2731
/// <param name="clock">Wall-clock source used for AFK hysteresis timestamps.</param>
2832
/// <param name="options">Timeouts/backoff/heartbeat settings.</param>
2933
/// <param name="logger">The logger.</param>
3034
internal sealed partial class ConnectionSupervisor(
3135
IRustSocketSource source,
3236
IServiceScopeFactory scopeFactory,
33-
IUserDmSender dmSender,
34-
ICredentialProtector protector,
37+
ConnectionSecurity security,
3538
IEventBus eventBus,
3639
IClock clock,
3740
IOptions<ConnectionOptions> options,
@@ -706,13 +709,13 @@ private async Task<IReadOnlyList<RigPosition>> GetRigPositionsAsync(
706709
string token;
707710
try
708711
{
709-
token = protector.Unprotect(active.ProtectedPlayerToken);
712+
token = security.Protector.Unprotect(active.ProtectedPlayerToken);
710713
}
711714
catch (CryptographicException ex)
712715
{
713716
LogUnreadableToken(logger, ex, active.Id);
714717
await store.MarkInvalidAsync(active.Id, ct).ConfigureAwait(false);
715-
await dmSender.SendAsync(
718+
await security.DmSender.SendAsync(
716719
active.OwnerUserId,
717720
$"Your Rust+ credential for **{server.Name}** could not be read — reconnect in #setup.",
718721
ct)
@@ -737,7 +740,7 @@ private async Task FailoverAsync(Guid credentialId, ulong ownerUserId, string se
737740
await store.MarkInvalidAsync(credentialId, ct).ConfigureAwait(false);
738741
}
739742

740-
await dmSender.SendAsync(
743+
await security.DmSender.SendAsync(
741744
ownerUserId,
742745
$"Your Rust+ credential for **{serverName}** was rejected — reconnect in #setup to keep it in the pool.",
743746
ct)

src/RustPlusBot.Features.Events/EventServiceCollectionExtensions.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ public static IServiceCollection AddEvents(this IServiceCollection services)
2727
services.AddSingleton<MarkerEventClassifier>();
2828
services.AddSingleton<EventEmbedRenderer>();
2929
services.AddSingleton<IEventChannelPoster, DiscordEventChannelPoster>();
30+
services.AddSingleton<EventRelayChannels>();
3031
services.AddSingleton<EventRelay>();
32+
services.AddSingleton<EventStores>();
3133
services.AddHostedService<EventsHostedService>();
3234

3335
return services;

src/RustPlusBot.Features.Events/Hosting/EventsHostedService.cs

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,23 @@
1212

1313
namespace RustPlusBot.Features.Events.Hosting;
1414

15+
/// <summary>Bundles the state-store collaborators injected into <see cref="EventsHostedService"/>.</summary>
16+
/// <param name="Store">The marker state store, cleared on disconnect.</param>
17+
/// <param name="RigStore">The rig state store, advanced by the tick and cleared on disconnect.</param>
18+
internal sealed record EventStores(EventStateStore Store, RigStateStore RigStore);
19+
1520
/// <summary>Runs the marker relay loop, the rig-event relay loop, the rig-timer tick, and the disconnect-clear loop.</summary>
1621
/// <param name="eventBus">The in-process event bus.</param>
1722
/// <param name="relay">Relays marker + rig events into Discord #events and in-game chat.</param>
18-
/// <param name="store">The marker state store, cleared on disconnect.</param>
19-
/// <param name="rigStore">The rig state store, advanced by the tick and cleared on disconnect.</param>
23+
/// <param name="stores">Bundles the marker and rig state stores.</param>
2024
/// <param name="clock">Supplies the current time for the tick.</param>
2125
/// <param name="options">Supplies the rig-tick interval.</param>
2226
/// <param name="scopeFactory">Opens scopes to read connection state.</param>
2327
/// <param name="logger">The logger.</param>
2428
internal sealed partial class EventsHostedService(
2529
IEventBus eventBus,
2630
EventRelay relay,
27-
EventStateStore store,
28-
RigStateStore rigStore,
31+
EventStores stores,
2932
IClock clock,
3033
IOptions<ConnectionOptions> options,
3134
IServiceScopeFactory scopeFactory,
@@ -77,7 +80,7 @@ public async Task StopAsync(CancellationToken cancellationToken)
7780
/// <returns>A task that completes when the tick has published all crossings.</returns>
7881
internal async Task TickOnceAsync(CancellationToken cancellationToken)
7982
{
80-
foreach (var c in rigStore.Advance(clock.UtcNow))
83+
foreach (var c in stores.RigStore.Advance(clock.UtcNow))
8184
{
8285
await eventBus.PublishAsync(
8386
new RigStateChangedEvent(c.GuildId, c.ServerId, c.Rig, c.Kind, c.X, c.Y, c.Dimensions),
@@ -184,8 +187,8 @@ private async Task ClearIfDisconnectedAsync(ConnectionStatusChangedEvent evt, Ca
184187
.ConfigureAwait(false);
185188
if (state is null || state.Status != ConnectionStatus.Connected)
186189
{
187-
store.Clear(evt.GuildId, evt.ServerId);
188-
rigStore.Clear(evt.GuildId, evt.ServerId);
190+
stores.Store.Clear(evt.GuildId, evt.ServerId);
191+
stores.RigStore.Clear(evt.GuildId, evt.ServerId);
189192
}
190193
}
191194
}

src/RustPlusBot.Features.Events/Relaying/EventRelay.cs

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,27 @@
1010

1111
namespace RustPlusBot.Features.Events.Relaying;
1212

13+
/// <summary>Bundles the channel/chat collaborators injected into <see cref="EventRelay"/>.</summary>
14+
/// <param name="Locator">Resolves the #events Discord channel id.</param>
15+
/// <param name="Poster">Posts embeds to the Discord channel.</param>
16+
/// <param name="TeamChatSender">Broadcasts the in-game team-chat line.</param>
17+
internal sealed record EventRelayChannels(
18+
IEventChannelLocator Locator,
19+
IEventChannelPoster Poster,
20+
ITeamChatSender TeamChatSender);
21+
1322
/// <summary>Posts every live event to #events AND in-game team chat; tracks rig state.</summary>
1423
/// <param name="classifier">Classifies raw marker deltas into domain events.</param>
1524
/// <param name="state">Tracks active markers and recent events per server.</param>
1625
/// <param name="renderer">Renders events as embeds and in-game lines.</param>
17-
/// <param name="locator">Resolves the #events Discord channel id.</param>
18-
/// <param name="poster">Posts embeds to the Discord channel.</param>
19-
/// <param name="teamChatSender">Broadcasts the in-game team-chat line.</param>
26+
/// <param name="channels">Bundles the channel/chat collaborators.</param>
2027
/// <param name="rigStore">Tracks oil-rig state (Apply on Activated).</param>
2128
/// <param name="scopeFactory">Opens scopes to read guild culture.</param>
2229
internal sealed class EventRelay(
2330
MarkerEventClassifier classifier,
2431
EventStateStore state,
2532
EventEmbedRenderer renderer,
26-
IEventChannelLocator locator,
27-
IEventChannelPoster poster,
28-
ITeamChatSender teamChatSender,
33+
EventRelayChannels channels,
2934
RigStateStore rigStore,
3035
IServiceScopeFactory scopeFactory)
3136
{
@@ -44,17 +49,18 @@ public async Task RelayAsync(MapMarkersChangedEvent evt, CancellationToken cance
4449
}
4550

4651
var culture = await GetCultureAsync(evt.GuildId, cancellationToken).ConfigureAwait(false);
47-
var channelId = await locator.GetChannelIdAsync(evt.GuildId, evt.ServerId, cancellationToken)
52+
var channelId = await channels.Locator.GetChannelIdAsync(evt.GuildId, evt.ServerId, cancellationToken)
4853
.ConfigureAwait(false);
4954

5055
foreach (var e in events)
5156
{
52-
await teamChatSender
57+
await channels.TeamChatSender
5358
.SendAsync(evt.GuildId, evt.ServerId, renderer.RenderLine(e, culture), cancellationToken)
5459
.ConfigureAwait(false);
5560
if (channelId is { } id)
5661
{
57-
await poster.PostAsync(id, renderer.Render(e, culture), cancellationToken).ConfigureAwait(false);
62+
await channels.Poster.PostAsync(id, renderer.Render(e, culture), cancellationToken)
63+
.ConfigureAwait(false);
5864
}
5965
}
6066
}
@@ -72,15 +78,16 @@ public async Task RelayRigAsync(RigStateChangedEvent evt, CancellationToken canc
7278
}
7379

7480
var culture = await GetCultureAsync(evt.GuildId, cancellationToken).ConfigureAwait(false);
75-
await teamChatSender
81+
await channels.TeamChatSender
7682
.SendAsync(evt.GuildId, evt.ServerId, renderer.RenderRigLine(evt, culture), cancellationToken)
7783
.ConfigureAwait(false);
7884

79-
var channelId = await locator.GetChannelIdAsync(evt.GuildId, evt.ServerId, cancellationToken)
85+
var channelId = await channels.Locator.GetChannelIdAsync(evt.GuildId, evt.ServerId, cancellationToken)
8086
.ConfigureAwait(false);
8187
if (channelId is { } id)
8288
{
83-
await poster.PostAsync(id, renderer.RenderRig(evt, culture), cancellationToken).ConfigureAwait(false);
89+
await channels.Poster.PostAsync(id, renderer.RenderRig(evt, culture), cancellationToken)
90+
.ConfigureAwait(false);
8491
}
8592
}
8693

0 commit comments

Comments
 (0)