You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Is your feature request related to a problem? Please describe.
The network analyzer (src/Network/Analyzer) is a WinForms tool which acts as a man-in-the-middle proxy. That works well for development, but an admin who wants to look at the traffic of a suspicious player usually can't set such a proxy up on the server machine. The traffic is already flowing through our own servers — we just don't expose it anywhere.
Status: phase 0 is done (#896), phase 1 is done (#897).
Describe the solution you'd like
A /network-analyzer page in the admin panel:
A collapsible connection list on the left, grouped by server type (connect server, game server, chat server) and by server instance.
An entry shows the account and character name where we know it, and falls back to the remote endpoint (IP:port) where we don't.
Selecting an entry starts capturing and shows the packets in a live-updating grid, with the same field extraction the WinForms tool does. The protocol version is applied automatically — no manual version dropdown.
An account flag IsNetworkObservationActive makes the game server archive the traffic of that account's sessions on the file system. Archived sessions are browsable in the same page — while the player is online and after they went offline — and can be deleted.
A button next to an online player (live map player list, online accounts page) navigates to the analyzer page with that connection preselected.
The WinForms tool stays. This is an addition, not a replacement — both use the same analysis code after the split described below.
All three server types create their client connections through the very same code path — MUnique.OpenMU.Network.Listener.CreateConnection returns a MUnique.OpenMU.Network.Connection:
GameServer/DefaultTcpGameServerListener.cs:81
ConnectServer/ClientListener.cs:67
ChatServer/ChatServerListener.cs:52
That is the lever for this feature: one capture hook in Connection covers all server types, and it sees the traffic decrypted:
incoming: Connection.ReadPacketAsync is called with one complete, already decrypted packet (the decryptor sits in front of it as Source);
outgoing: Connection.Output is an ExtendedPipeWriter which wraps the encryptor's writer, so everything written to it is still plaintext.
2. Design
2.1 Capture hook in MUnique.OpenMU.Network — done in #897
/// Sink for captured, decrypted packets of a connection.publicinterfaceIPacketCaptureSink{voidPacketCaptured(ReadOnlySpan<byte>packet,boolsent);}
IConnection got a Guid Id — a stable handle for the UI routes and useful for log correlation.
Sinks are registered with IConnection.AddCaptureSink and removed with RemoveCaptureSink. They are kept in an ImmutableArray updated with ImmutableInterlocked, so there is no lock and iterating them per packet stays allocation free. As long as none is registered, a connection captures nothing.
Incoming packets are handed to the sinks in Connection.ReadPacketAsync — they arrive as complete packets already.
Outgoing data doesn't: a write to the pipe writer is not necessarily one packet. It is therefore copied into an own pipe, which is read by a CapturedPacketReader : PacketPipeReaderBase — reusing the packet splitting of the network layer instead of a bespoke assembler.
A capture may only start or end at a packet boundary. The ExtendedPipeWriter knows where those are, so it applies a requested change when nothing is written to the target yet or right after a flush.
The capture never blocks or breaks the connection: no writer backpressure on its pipe, failing capture writes are swallowed, and malformed captured data stops the capture (not the connection).
src/Network/Analyzer/MUnique.OpenMU.Network.Analyzer.csproj — net10.0, platform neutral: Packet, PacketAnalyzer, FieldExtensions, ICapturedConnection, SavedConnection, CapturedConnectionExtensions, plus the archive types of phase 5.
src/Network/Analyzer.WinForms/MUnique.OpenMU.Network.Analyzer.WinForms.csproj — the WinForms app, net10.0-windows, referencing the library.
Three changes inside PacketAnalyzer (done in #897):
ClientVersion is a parameter of ExtractInformation / ExtractShortInformation instead of instance state, so one shared instance can serve connections with different client versions concurrently. This is what makes the automatic version handling below possible.
Definitions are keyed by a "definition set" instead of "one file per direction", and the direction is taken from PacketDefinition.Direction. That enables reusing ConnectServer/ConnectServerPackets.xml and ChatServer/ChatServerPackets.xml for the other two server types.
The FileSystemWatcher-based hot reload is opt-in via a ctor flag and stays off in the server hosts — editing packet definitions at runtime is a rare development activity, so it remains WinForms-only.
The XML files have to be copied to the output of the hosts which render the admin panel (src/Startup, src/Dapr/AdminPanel.Host); the MUnique.OpenMU.Network.Packets project already publishes them as content.
2.3 Capture service, server adapters and the automatic protocol version
publicinterfaceICapturedConnectionInfo// one entry in the left list{GuidId{get;}ServerTypeServerType{get;}intServerId{get;}string?AccountName{get;}string?CharacterName{get;}EndPoint?RemoteEndPoint{get;}ClientVersionClientVersion{get;}// kept up to date, see belowDateTimeConnectedAt{get;}boolIsCapturing{get;}boolIsObserved{get;}stringDisplayName{get;}// character ?? account ?? endpoint}publicinterfaceIPacketCaptureService// singleton{IReadOnlyList<ICapturedConnectionInfo>Connections{get;}eventEventHandler?ConnectionsChanged;ValueTask<ILiveCapturedConnection>StartCaptureAsync(GuidconnectionId);ValueTaskStopCaptureAsync(GuidconnectionId);ValueTaskDisconnectAsync(GuidconnectionId);ValueTaskSetObservationAsync(stringaccountName,boolactive);ICapturedConnectionInfo?Find(intserverId,stringplayerOrAccountName);}
ILiveCapturedConnection : ICapturedConnection holds the captured packets in a capped buffer (size from SystemConfiguration, default 5,000 packets, oldest dropped) and raises a change event. It is a sink of the connection.
The service aggregates IConnectionSource implementations registered in DI — one per server type, living in the respective server project so that no internals have to be made public to the web layer:
Server
Source of the connections
Name resolution
Protocol version
Game server
IGameContext.GetPlayersAsync() — players are added on connect (GameServer.OnPlayerConnectedAsync), so connections at the login screen are included
RemotePlayer implements IClientVersionProvider: initial value from the endpoint's GameClientDefinition, updated in LogInHandlerPlugIn (ClientVersionChanged event)
Connect server
ClientListener.Clients (needs a small accessor on IConnectServer)
none — endpoint only
ConnectServer.ClientVersion from its settings
Chat server
ChatServer._connectedClients (needs an accessor on IChatServer)
IChatClient.Nickname, room id
version independent — the chat definition set
So the version is never asked for in the UI: the connection knows it, the source publishes it, the analyzer gets it per call. When it changes mid-session (0.75 default → the real version detected at login), the source raises a change and the already displayed rows are re-analyzed.
GameContext currently has no event when a player is added/removed (GameMapCreated/GameMapRemoved exist, players don't). Add PlayerAdded/PlayerRemoved events in GameContext.AddPlayerAsync / RemovePlayerAsync so the connection list can update push-based instead of polling.
No backfill for ad-hoc captures. Capture starts when a connection is selected; earlier packets of that connection are gone. The observation feature below is the answer when the full picture is needed.
2.4 Account observation and the session archive
The flag.MUnique.OpenMU.DataModel.Entities.Account gets
publicboolIsNetworkObservationActive{get;set;}
modelled exactly like the existing IsBot / IsVaultExtended. The EF model class is generated from the data model, so this needs one new migration (see Persistence/EntityFramework/Migrations/Cheatsheet.md). EditAccount renders it automatically through AutoForm.
Toggling it from the analyzer page. The connection header gets a toggle which calls IPacketCaptureService.SetObservationAsync. That persists the flag on the account and applies it to the live player object, so it takes effect immediately without a reconnect.
When archiving starts. The game server connection source subscribes to player.PlayerState.StateChanged. When a player reaches the logged-in state, player.Account.IsNetworkObservationActive is checked and, if set, an archive writer is registered as a second sink of the connection. Consequence worth stating: the few packets exchanged before login (version check, handshake) are not part of the archive. Capturing those would require buffering every connection's first packets unconditionally, which contradicts the "no always-on capture" rule.
Archiving is a game server feature: the connect server has no account, the chat server only a nickname. Both are still listed live.
Where it runs. The archive writer belongs to the game server, not to the admin panel: it is registered in the all-in-one host and in Dapr/GameServer.Host, so an observed account is archived even when no admin panel is running (see the deployment note below).
Format. Reuse the existing .mucap CSV (timestampTicks;toServer;size;hexBytes) so files stay loadable by the WinForms tool, with two additions:
a monotonically increasing per-session sequence number appended as a fifth field — the existing loader reads only fields 0..3 and ignores the rest, so this stays backwards compatible. It makes merging "archived so far" with the live tail exact (no gap, no duplicate).
a sidecar .json with the metadata: account, character(s), server type/id, remote endpoint, start/end time, packet count, and the client version including its change points (sequence number → version), so a replay analyzes the early packets with the version that was actually in effect.
The writer is a streaming, append-only, periodically flushed writer (not save-at-the-end), so a session is readable while it runs and survives a crash. Readers open with FileShare.ReadWrite | FileShare.Delete.
Layout and housekeeping.<archivePath>/<accountName>/<yyyyMMdd-HHmmss>_<serverType><serverId>_<connectionId>.mucap plus the sidecar. Archive path, rotation size, quota and retention come from SystemConfiguration. The archive directory must not be inside the statically served logs folder.
2.5 The Blazor UI
New folder src/Web/AdminPanel/Components/NetworkAnalyzer/:
Component
Job
Pages/NetworkAnalyzer.razor
routes /network-analyzer, /network-analyzer/{ConnectionId:guid} and /network-analyzer/archive/{SessionId}; two-column layout
ConnectionList.razor
left column: Bootstrap accordion; "Live connections" grouped by server type → server instance, and "Archived sessions" grouped by account → session; search box; per live entry: display name, endpoint, packet counter, record toggle, disconnect button; per archived entry: date, duration, packet count, size, open/download/delete
PacketGrid.razor
QuickGrid with Virtualize; columns: timestamp, direction (C→S / S→C), size, code (hex), short message
PacketDetail.razor
extracted fields (PacketAnalyzer.ExtractInformation) plus a hex dump of the selected packet
AnalyzerToolbar.razor
pause/resume, autoscroll, clear, direction filter, text filter on code/name, observation toggle, download .mucap
NetworkAnalyzerViewModel.cs
scoped (per circuit) state: subscribes to the service, throttles the render via the existing Debouncer (e.g. max 10 updates/s), holds filter and selection, merges archive + live tail by sequence number
Details worth deciding early:
Capture lifetime. An ad-hoc capture starts when a connection is selected and stops when the last viewer deselects it or the circuit is disposed (reference counted in the service). An archive capture is independent of any viewer and lives as long as the session.
Opening an observed player shows the archived packets of the running session first, then continues live.
Opening an archived session of an offline player is a pure file read; large files are read paged/tail-first with a cap, and the grid is virtualized.
Live update. Packets arrive on network threads, so the view model snapshots under a lock and marshals via InvokeAsync(StateHasChanged).
Deep link.Pages/LoggedIn.razor and Web/Map/Components/MapPlayerList.razor get a button navigating to network-analyzer/{id}; the id is resolved via IPacketCaptureService.Find(serverId, name) so the web layer never touches a Connection. MapPlayerList lives in the Map project, so the route is passed in via a cascading parameter like LiveMapRoute already does, keeping the Map project standalone-buildable.
Menu. New entry in Components/Layout/NavMenu.razor, only rendered when an IPacketCaptureService is registered — the same graceful degradation the global message card in Servers.razor uses.
Localization. All new strings go into Web/AdminPanel/Properties/Resources.resx.
2.6 Settings in the database
New properties on DataModel/Configuration/SystemConfiguration.cs (with Display attributes and resource entries, like the existing ones), covered by the same migration as the account flag:
Property
Default
Purpose
NetworkAnalyzerLiveBufferSize
5000
packets kept per live capture
NetworkObservationArchivePath
captures
archive root, relative to the app directory
NetworkObservationMaxSessionSizeMb
50
rotation threshold per session file
NetworkObservationMaxTotalSizeMb
1000
quota; oldest sessions removed first
NetworkObservationRetentionDays
30
age-based cleanup
They are editable in the admin panel's system configuration form for free.
3. Suggested phases
Each phase is meant to be a separate, self-contained PR.
Phase 2 — capture service + server adapters. §2.3, including the PlayerAdded/PlayerRemoved events, the accessors on IConnectServer / IChatServer, and the automatic client version.
Phase 3 — the page (live). §2.5 for live connections only.
Phase 4 — deep links. Buttons in LoggedIn.razor and MapPlayerList.razor.
Phase 5 — observation and archive. §2.4 and §2.6: account flag + migration, SystemConfiguration entries, streaming writer, sidecar metadata, rotation/quota/retention, registration in the all-in-one host and in Dapr/GameServer.Host. Tests for the writer/reader round-trip (including a file which is being written while read) and for the housekeeping.
Phase 6 — archive browser. The "Archived sessions" part of the sidebar: list, open (offline and running), download, delete; observation toggle in the page.
Describe alternatives you've considered
Keeping the MITM proxy as the only option. Rejected as the sole solution because an admin typically can't deploy it on the server; the WinForms tool stays for development.
Wrapping the whole IConnection in a capturing decorator at listener level. Rejected because capture must be switchable at runtime for an already-connected player, which a decorator installed at accept time can't do without paying the cost for every connection.
Always buffering the first packets of every connection so that the pre-login traffic of an observed account can be archived retroactively. Rejected — it contradicts "capture only what's asked for" for a gain of two or three handshake packets.
A new capture file format. Rejected in favour of extending .mucap with a fifth CSV field plus a sidecar .json, which keeps the files loadable by the existing WinForms tool.
A hand written packet assembler for the outgoing data. Replaced during phase 1 by an own pipe plus a PacketPipeReaderBase, which reuses the packet splitting we already have.
Additional context
Decisions:
Offline/AFK-trade players are not captured — they have no connection.
Observation is an account setting, archived per session on the file system, browsable online and offline, deletable.
Analyzer settings live in SystemConfiguration in the database and are editable in the admin panel.
The WinForms tool stays as a development tool; this is an addition.
The protocol version is derived from the connection, never chosen manually.
No backfill for ad-hoc captures; the observation archive covers that need.
No packet definition file watcher in the server hosts.
The chat server traffic of an observed account is not archived for now. It would need a mapping from the chat authentication token back to the account; the chat connections are still listed live.
An observed session is archived independently of the admin panel: the writer is registered in the game server host, so a distributed deployment archives the traffic of observed accounts as well.
The packet sender is not ported to the page. It stays a WinForms-only development tool.
Data protection. Admin panel authentication is tracked separately, but two properties of this feature outlive that issue: captures and archives contain the plaintext login packet, so the archive directory must not be served statically (unlike logs) and archived files should be created with restrictive permissions; and starting/stopping a capture, toggling the observation flag and deleting an archived session should be logged at information level, so observing a player leaves a trace.
Deployment. Like the live map, the page works in the all-in-one deployment only, because the capture service needs the server objects in-process. In the Dapr deployment the service is simply not registered and the menu entry is hidden. The archive is not restricted that way (decision 9): it lives in the game server, so observed accounts are archived in a distributed deployment too — the sessions are then browsable once a page can reach that host. A later iteration could stream live captures from the server hosts to the admin panel host (SignalR or the existing Dapr channels) without changing the interfaces above, since they are already async and id-based.
Is your feature request related to a problem? Please describe.
The network analyzer (
src/Network/Analyzer) is a WinForms tool which acts as a man-in-the-middle proxy. That works well for development, but an admin who wants to look at the traffic of a suspicious player usually can't set such a proxy up on the server machine. The traffic is already flowing through our own servers — we just don't expose it anywhere.Status: phase 0 is done (#896), phase 1 is done (#897).
Describe the solution you'd like
A
/network-analyzerpage in the admin panel:IsNetworkObservationActivemakes the game server archive the traffic of that account's sessions on the file system. Archived sessions are browsable in the same page — while the player is online and after they went offline — and can be deleted.The WinForms tool stays. This is an addition, not a replacement — both use the same analysis code after the split described below.
1. Starting point
1.1 The analyzer tool
Only a few of its 15 files are actually WinForms:
MainForm.*,PacketSenderForm.*,Program.csLiveConnection.cs,LiveConnectionListener.cs,ClientConnectedEventArgs.csPacketAnalyzer.csPacket.cs,ICapturedConnection.cs,SavedConnection.cs,CapturedConnectionExtensions.cs,FieldExtensions.cs.mucapsave/load1.2 The servers
All three server types create their client connections through the very same code path —
MUnique.OpenMU.Network.Listener.CreateConnectionreturns aMUnique.OpenMU.Network.Connection:GameServer/DefaultTcpGameServerListener.cs:81ConnectServer/ClientListener.cs:67ChatServer/ChatServerListener.cs:52That is the lever for this feature: one capture hook in
Connectioncovers all server types, and it sees the traffic decrypted:Connection.ReadPacketAsyncis called with one complete, already decrypted packet (the decryptor sits in front of it asSource);Connection.Outputis anExtendedPipeWriterwhich wraps the encryptor's writer, so everything written to it is still plaintext.2. Design
2.1 Capture hook in
MUnique.OpenMU.Network— done in #897IConnectiongot aGuid Id— a stable handle for the UI routes and useful for log correlation.IConnection.AddCaptureSinkand removed withRemoveCaptureSink. They are kept in anImmutableArrayupdated withImmutableInterlocked, so there is no lock and iterating them per packet stays allocation free. As long as none is registered, a connection captures nothing.Connection.ReadPacketAsync— they arrive as complete packets already.CapturedPacketReader : PacketPipeReaderBase— reusing the packet splitting of the network layer instead of a bespoke assembler.ExtendedPipeWriterknows where those are, so it applies a requested change when nothing is written to the target yet or right after a flush.2.2 Analyzer library split — done in #896
src/Network/Analyzer/MUnique.OpenMU.Network.Analyzer.csproj—net10.0, platform neutral:Packet,PacketAnalyzer,FieldExtensions,ICapturedConnection,SavedConnection,CapturedConnectionExtensions, plus the archive types of phase 5.src/Network/Analyzer.WinForms/MUnique.OpenMU.Network.Analyzer.WinForms.csproj— the WinForms app,net10.0-windows, referencing the library.Three changes inside
PacketAnalyzer(done in #897):ClientVersionis a parameter ofExtractInformation/ExtractShortInformationinstead of instance state, so one shared instance can serve connections with different client versions concurrently. This is what makes the automatic version handling below possible.PacketDefinition.Direction. That enables reusingConnectServer/ConnectServerPackets.xmlandChatServer/ChatServerPackets.xmlfor the other two server types.FileSystemWatcher-based hot reload is opt-in via a ctor flag and stays off in the server hosts — editing packet definitions at runtime is a rare development activity, so it remains WinForms-only.The XML files have to be copied to the output of the hosts which render the admin panel (
src/Startup,src/Dapr/AdminPanel.Host); theMUnique.OpenMU.Network.Packetsproject already publishes them as content.2.3 Capture service, server adapters and the automatic protocol version
ILiveCapturedConnection : ICapturedConnectionholds the captured packets in a capped buffer (size fromSystemConfiguration, default 5,000 packets, oldest dropped) and raises a change event. It is a sink of the connection.The service aggregates
IConnectionSourceimplementations registered in DI — one per server type, living in the respective server project so that no internals have to be made public to the web layer:IGameContext.GetPlayersAsync()— players are added on connect (GameServer.OnPlayerConnectedAsync), so connections at the login screen are includedplayer.Account?.LoginName,player.SelectedCharacter?.Name, live-updatedRemotePlayerimplementsIClientVersionProvider: initial value from the endpoint'sGameClientDefinition, updated inLogInHandlerPlugIn(ClientVersionChangedevent)ClientListener.Clients(needs a small accessor onIConnectServer)ConnectServer.ClientVersionfrom its settingsChatServer._connectedClients(needs an accessor onIChatServer)IChatClient.Nickname, room idSo the version is never asked for in the UI: the connection knows it, the source publishes it, the analyzer gets it per call. When it changes mid-session (0.75 default → the real version detected at login), the source raises a change and the already displayed rows are re-analyzed.
GameContextcurrently has no event when a player is added/removed (GameMapCreated/GameMapRemovedexist, players don't). AddPlayerAdded/PlayerRemovedevents inGameContext.AddPlayerAsync/RemovePlayerAsyncso the connection list can update push-based instead of polling.No backfill for ad-hoc captures. Capture starts when a connection is selected; earlier packets of that connection are gone. The observation feature below is the answer when the full picture is needed.
2.4 Account observation and the session archive
The flag.
MUnique.OpenMU.DataModel.Entities.Accountgetsmodelled exactly like the existing
IsBot/IsVaultExtended. The EF model class is generated from the data model, so this needs one new migration (seePersistence/EntityFramework/Migrations/Cheatsheet.md).EditAccountrenders it automatically throughAutoForm.Toggling it from the analyzer page. The connection header gets a toggle which calls
IPacketCaptureService.SetObservationAsync. That persists the flag on the account and applies it to the live player object, so it takes effect immediately without a reconnect.When archiving starts. The game server connection source subscribes to
player.PlayerState.StateChanged. When a player reaches the logged-in state,player.Account.IsNetworkObservationActiveis checked and, if set, an archive writer is registered as a second sink of the connection. Consequence worth stating: the few packets exchanged before login (version check, handshake) are not part of the archive. Capturing those would require buffering every connection's first packets unconditionally, which contradicts the "no always-on capture" rule.Archiving is a game server feature: the connect server has no account, the chat server only a nickname. Both are still listed live.
Where it runs. The archive writer belongs to the game server, not to the admin panel: it is registered in the all-in-one host and in
Dapr/GameServer.Host, so an observed account is archived even when no admin panel is running (see the deployment note below).Format. Reuse the existing
.mucapCSV (timestampTicks;toServer;size;hexBytes) so files stay loadable by the WinForms tool, with two additions:.jsonwith the metadata: account, character(s), server type/id, remote endpoint, start/end time, packet count, and the client version including its change points (sequence number → version), so a replay analyzes the early packets with the version that was actually in effect.The writer is a streaming, append-only, periodically flushed writer (not save-at-the-end), so a session is readable while it runs and survives a crash. Readers open with
FileShare.ReadWrite | FileShare.Delete.Layout and housekeeping.
<archivePath>/<accountName>/<yyyyMMdd-HHmmss>_<serverType><serverId>_<connectionId>.mucapplus the sidecar. Archive path, rotation size, quota and retention come fromSystemConfiguration. The archive directory must not be inside the statically servedlogsfolder.2.5 The Blazor UI
New folder
src/Web/AdminPanel/Components/NetworkAnalyzer/:Pages/NetworkAnalyzer.razor/network-analyzer,/network-analyzer/{ConnectionId:guid}and/network-analyzer/archive/{SessionId}; two-column layoutConnectionList.razorPacketGrid.razorQuickGridwithVirtualize; columns: timestamp, direction (C→S / S→C), size, code (hex), short messagePacketDetail.razorPacketAnalyzer.ExtractInformation) plus a hex dump of the selected packetAnalyzerToolbar.razor.mucapNetworkAnalyzerViewModel.csDebouncer(e.g. max 10 updates/s), holds filter and selection, merges archive + live tail by sequence numberDetails worth deciding early:
InvokeAsync(StateHasChanged).Pages/LoggedIn.razorandWeb/Map/Components/MapPlayerList.razorget a button navigating tonetwork-analyzer/{id}; the id is resolved viaIPacketCaptureService.Find(serverId, name)so the web layer never touches aConnection.MapPlayerListlives in the Map project, so the route is passed in via a cascading parameter likeLiveMapRoutealready does, keeping the Map project standalone-buildable.Components/Layout/NavMenu.razor, only rendered when anIPacketCaptureServiceis registered — the same graceful degradation the global message card inServers.razoruses.Web/AdminPanel/Properties/Resources.resx.2.6 Settings in the database
New properties on
DataModel/Configuration/SystemConfiguration.cs(withDisplayattributes and resource entries, like the existing ones), covered by the same migration as the account flag:NetworkAnalyzerLiveBufferSizeNetworkObservationArchivePathcapturesNetworkObservationMaxSessionSizeMbNetworkObservationMaxTotalSizeMbNetworkObservationRetentionDaysThey are editable in the admin panel's system configuration form for free.
3. Suggested phases
Each phase is meant to be a separate, self-contained PR.
Phase 0 — analyzer library split.Done in Split the network analyzer into a library and the WinForms tool #896.Phase 1 — capture infrastructure.Done in Add packet capturing to the connections and make the analyzer reusable #897, including the definition-set, client-version and file-watcher changes ofPacketAnalyzer.PlayerAdded/PlayerRemovedevents, the accessors onIConnectServer/IChatServer, and the automatic client version.LoggedIn.razorandMapPlayerList.razor.SystemConfigurationentries, streaming writer, sidecar metadata, rotation/quota/retention, registration in the all-in-one host and inDapr/GameServer.Host. Tests for the writer/reader round-trip (including a file which is being written while read) and for the housekeeping.Describe alternatives you've considered
IConnectionin a capturing decorator at listener level. Rejected because capture must be switchable at runtime for an already-connected player, which a decorator installed at accept time can't do without paying the cost for every connection..mucapwith a fifth CSV field plus a sidecar.json, which keeps the files loadable by the existing WinForms tool.PacketPipeReaderBase, which reuses the packet splitting we already have.Additional context
Decisions:
SystemConfigurationin the database and are editable in the admin panel.Data protection. Admin panel authentication is tracked separately, but two properties of this feature outlive that issue: captures and archives contain the plaintext login packet, so the archive directory must not be served statically (unlike
logs) and archived files should be created with restrictive permissions; and starting/stopping a capture, toggling the observation flag and deleting an archived session should be logged at information level, so observing a player leaves a trace.Deployment. Like the live map, the page works in the all-in-one deployment only, because the capture service needs the server objects in-process. In the Dapr deployment the service is simply not registered and the menu entry is hidden. The archive is not restricted that way (decision 9): it lives in the game server, so observed accounts are archived in a distributed deployment too — the sessions are then browsable once a page can reach that host. A later iteration could stream live captures from the server hosts to the admin panel host (SignalR or the existing Dapr channels) without changing the interfaces above, since they are already async and id-based.