Files
Rainer LeitandClaude Fable 5.1 3e52d26fb7 Step 2: the telemetry seam
ITelemetrySink with the null, log and JSON Lines sinks, FTelemetryEvent and the envelope, TelemetryEvents,
UTelemetrySubsystem on the game instance, ASaltyGameMode minting the session id into the replicated
ASaltyGameState, bs.TelemetryTest, the git hash in the build string (D-42) and four Salty.Core.Telemetry tests
replacing the placeholder. Proved standalone and with a headless server plus client sharing one session id.

Also enables the engine's Editor, AutomationTest, GameplayTags, ConfigSettings and LiveCoding MCP toolsets
(D-43) so the editor's MCP server exposes more than the skills toolset.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 20:22:20 +03:00

60 lines
3.0 KiB
C++

#pragma once
#include "CoreMinimal.h"
#include "Dom/JsonObject.h"
// One emission. Call sites fill Name (from TelemetryEvents) and Payload; the subsystem stamps the envelope.
struct SALTYCORE_API FTelemetryEvent
{
FName Name;
TSharedPtr<FJsonObject> Payload;
};
// Stamped by the subsystem onto every event. Serialised with snake_case keys; see Telemetry::ToJsonLine.
struct SALTYCORE_API FTelemetryEnvelope
{
int32 SchemaVersion = 1;
FGuid EventId; // unique per emission, for de-duplication
FDateTime TimestampUtc;
float GameTime = 0.f; // seconds since the world began; zero when there is no world yet
FGuid SessionId; // minted by the server, adopted by clients on join; invalid before a session
FString PlayerId; // hashed server-minted id; empty on the server
bool bIsServer = false; // authoritative events versus observed ones
int32 PartySize = 0;
FString Build; // FApp::GetBuildVersion() plus the git hash
FString Map;
bool bCheatsUsed = false; // sticky true after the first cheat this session
};
// A small fluent builder for payloads, so a call site reads as one expression:
// Telemetry->Emit(TelemetryEvents::CheatUsed, FTelemetryPayload().Set(TEXT("command"), Command));
class SALTYCORE_API FTelemetryPayload
{
public:
FTelemetryPayload() : Object(MakeShared<FJsonObject>()) {}
FTelemetryPayload& Set(const FString& Key, const FString& Value) { Object->SetStringField(Key, Value); return *this; }
FTelemetryPayload& Set(const FString& Key, const TCHAR* Value) { Object->SetStringField(Key, Value); return *this; }
FTelemetryPayload& Set(const FString& Key, FName Value) { Object->SetStringField(Key, Value.ToString()); return *this; }
FTelemetryPayload& Set(const FString& Key, const FGuid& Value) { Object->SetStringField(Key, Value.ToString(EGuidFormats::DigitsWithHyphensLower)); return *this; }
FTelemetryPayload& Set(const FString& Key, double Value) { Object->SetNumberField(Key, Value); return *this; }
FTelemetryPayload& Set(const FString& Key, float Value) { Object->SetNumberField(Key, Value); return *this; }
FTelemetryPayload& Set(const FString& Key, int32 Value) { Object->SetNumberField(Key, Value); return *this; }
FTelemetryPayload& Set(const FString& Key, int64 Value) { Object->SetNumberField(Key, static_cast<double>(Value)); return *this; }
FTelemetryPayload& Set(const FString& Key, bool Value) { Object->SetBoolField(Key, Value); return *this; }
TSharedPtr<FJsonObject> Build() const { return Object; }
operator TSharedPtr<FJsonObject>() const { return Object; }
private:
TSharedPtr<FJsonObject> Object;
};
namespace Telemetry
{
// The one serialisation of an event: the envelope's fields as snake_case keys, then "name" and "payload"
// (an empty object when the event has none), condensed to a single line with no trailing newline. Pure, so
// the sinks share it and the tests can check it without a sink.
SALTYCORE_API FString ToJsonLine(const FTelemetryEnvelope& Envelope, const FTelemetryEvent& Event);
}