Skip to content

Commit abff3eb

Browse files
HandyS11claude
andcommitted
docs: design for splitting player events into a #player-events channel
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 00039cc commit abff3eb

1 file changed

Lines changed: 144 additions & 0 deletions

File tree

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
# Player Events Channel — Design
2+
3+
**Date:** 2026-07-22
4+
**Status:** Approved
5+
6+
## Problem
7+
8+
Every live notification lands in the same per-server `#events` channel: map events (cargo
9+
ship, patrol helicopter, chinook, oil rigs), wipe announcements, and team-player presence
10+
transitions (login, logout, death, AFK). The player noise drowns the map events.
11+
12+
## Goal
13+
14+
Route team-player presence transitions to a new per-server `#player-events` channel. Map
15+
events and wipe announcements stay in `#events`. In-game team-chat messaging is unchanged
16+
for both feeds.
17+
18+
## Scope
19+
20+
**In scope:** `PlayerStateChangedEvent` (the presence transitions published by the team-info
21+
poll) — the only event `Features.Players` relays.
22+
23+
**Out of scope:** `MapMarkersChangedEvent` / `RigStateChangedEvent` (`Features.Events`),
24+
wipe announcements (`Features.Wipes`), the pinned `#info` team roster embed, smart alarms
25+
and switches (already have their own channels). None of these change.
26+
27+
## Current Architecture
28+
29+
Three relays resolve the same locator to the same channel:
30+
31+
| Consumer | Feature project | Locator | Channel |
32+
|---|---|---|---|
33+
| `EventRelay` | `Features.Events` | `IEventChannelLocator` | `#events` |
34+
| `WipeAnnouncer` | `Features.Wipes` | `IEventChannelLocator` | `#events` |
35+
| `PlayerEventRelay` | `Features.Players` | `IEventChannelLocator` | `#events` |
36+
37+
Channels are declarative: `ServerWorkspaceSpecProvider` returns `ChannelSpec` records, and
38+
`WorkspaceReconciler.EnsureChannelsAsync` creates any missing channel and enforces the
39+
declared ordering via `EnsureChannelOrderAsync`. Locators are singletons subclassing
40+
`CachingChannelLocator` (30s TTL over `IWorkspaceStore.GetChannelsByKeyAsync`).
41+
42+
Player events already live in their own feature project, so the split is a new channel key
43+
plus a new locator — not a code move.
44+
45+
## Approach
46+
47+
Mirror the existing locator pattern, which the codebase already uses nine times over.
48+
49+
Rejected alternatives:
50+
51+
- **Parameterize one locator by channel key.** Would collapse nine near-identical locator
52+
classes into one, but touches every locator and consumer. Unrelated refactor.
53+
- **Single channel with Discord tags/threads.** Does not meet the goal.
54+
55+
## Changes
56+
57+
### 1. Channel key
58+
59+
`WorkspaceKeys.cs` — add to `WorkspaceChannelKeys`:
60+
61+
```csharp
62+
/// <summary>Key for the per-server #player-events channel.</summary>
63+
public const string ServerPlayerEvents = "playerevents";
64+
```
65+
66+
### 2. Channel spec
67+
68+
`ServerWorkspaceSpecProvider.GetChannelSpecs()` — insert after `ServerEvents` at position 5,
69+
shifting `ServerMap`, `ServerSwitches`, `ServerAlarms`, `ServerStorageMonitors` to 6–9:
70+
71+
```csharp
72+
new(WorkspaceScope.PerServer, WorkspaceChannelKeys.ServerPlayerEvents, "channel.playerevents.name",
73+
ChannelPermissionProfile.ReadOnly, 5),
74+
```
75+
76+
Read-only, like `#events` — the bot posts, players do not.
77+
78+
### 3. Localized names
79+
80+
`Strings.resx`: `channel.playerevents.name` = `player-events`
81+
`Strings.fr.resx`: `channel.playerevents.name` = `evenements-joueurs`
82+
83+
### 4. Locator
84+
85+
`Features.Workspace/Locating/IPlayerEventChannelLocator.cs` and
86+
`PlayerEventChannelLocator.cs`, modelled exactly on `IEventChannelLocator` /
87+
`EventChannelLocator`:
88+
89+
```csharp
90+
internal sealed class PlayerEventChannelLocator(IServiceScopeFactory scopeFactory, IClock clock)
91+
: CachingChannelLocator(scopeFactory, clock, WorkspaceChannelKeys.ServerPlayerEvents),
92+
IPlayerEventChannelLocator;
93+
```
94+
95+
Registered in `WorkspaceServiceCollectionExtensions` alongside the other locators:
96+
97+
```csharp
98+
services.AddSingleton<IPlayerEventChannelLocator, PlayerEventChannelLocator>();
99+
```
100+
101+
### 5. Routing
102+
103+
`PlayerEventRelay` swaps its `IEventChannelLocator locator` constructor parameter for
104+
`IPlayerEventChannelLocator locator`. The XML doc comment updates from "#events" to
105+
"#player-events". Nothing else in the class changes — in particular the
106+
`teamChatSender.SendAsync(...)` call, the renderer calls, and the per-transition loop are
107+
untouched, so the in-game team-chat output is byte-identical.
108+
109+
`EventRelay` and `WipeAnnouncer` are not modified.
110+
111+
## Data Flow
112+
113+
```
114+
Team-info poll → PlayerStateChangedEvent → PlayersHostedService → PlayerEventRelay
115+
├─ teamChatSender.SendAsync(...) → in-game team chat (unchanged)
116+
└─ IPlayerEventChannelLocator.GetChannelIdAsync(...) → #player-events (new target)
117+
118+
Map markers → MapMarkersChangedEvent → EventRelay
119+
├─ teamChatSender.SendAsync(...) → in-game team chat (unchanged)
120+
└─ IEventChannelLocator.GetChannelIdAsync(...) → #events (unchanged)
121+
```
122+
123+
## Migration
124+
125+
None required. On the next reconcile, `WorkspaceReconciler` creates `#player-events` in every
126+
existing per-server category and reorders the category to match the declared positions.
127+
Historical messages already in `#events` stay there.
128+
129+
## Error Handling
130+
131+
`PlayerEventRelay` already treats a null channel id as "skip the Discord post" while still
132+
sending the in-game line. That covers the window between deploy and first reconcile, and any
133+
case where a guild admin deletes the channel. No fallback to `#events` — a temporary gap
134+
matches how every other channel behaves on introduction.
135+
136+
## Testing
137+
138+
| Test | Assertion |
139+
|---|---|
140+
| `PlayerEventRelayTests` | Substitutes `IPlayerEventChannelLocator`; player embeds post to the player-events channel id and never to `#events`. In-game line still sent when the locator returns null. |
141+
| `PlayersHostedServiceTests`, `PlayerEventRegistrationTests` | Updated to the new interface; DI graph resolves. |
142+
| `WorkspaceRegistrationTests` | `IPlayerEventChannelLocator` is registered. |
143+
| `ServerWorkspaceSpecProvider` spec test | `playerevents` spec exists at position 5, read-only; `events` remains at position 4 and the four following channels are at 6–9. |
144+
| `EventRelayTests`, `WipeAnnouncerTests` | Unchanged — regression proof that map events and wipe announcements still target `#events`. |

0 commit comments

Comments
 (0)