Add SCUM log sessions and trajectory projections

This commit is contained in:
npc0-hue
2026-08-27 12:34:07 +08:00
parent 0940780058
commit 316efbe780
38 changed files with 1549 additions and 84 deletions
@@ -3,10 +3,13 @@ package companion
import (
"crypto/sha256"
"encoding/hex"
"regexp"
"strings"
"time"
)
var scumLoginLogLine = regexp.MustCompile(`^\d{4}\.\d{2}\.\d{2}-\d{2}\.\d{2}\.\d{2}: '([0-9.]+) (\d{1,50}):([^']{1,80})\(\d+\)' logged (in|out)(?: .*)?$`)
// ConsoleRecord is supplied by Run's stdout/stderr stream, not by the server
// execution log. The channel never accepts a file path or a raw log archive.
type ConsoleRecord struct {
@@ -21,6 +24,7 @@ type SemanticEvent struct {
Sequence uint64
Type string
PlayerID string
DisplayName string
OccurredAt time.Time
NetworkCorrelation string
}
@@ -62,6 +66,9 @@ func ParseConsoleRecords(serverID string, records []ConsoleRecord, correlationSe
return batch
}
func parseConsoleRecord(record ConsoleRecord, secret string) (SemanticEvent, bool) {
if event, ok := parseLoginLogRecord(record, secret); ok {
return event, true
}
fields := strings.Fields(record.Text)
if len(fields) < 3 || fields[0] != "SCUM" || (fields[1] != "LOGIN" && fields[1] != "LOGOUT") || !steamID64(fields[2]) {
return SemanticEvent{}, false
@@ -76,9 +83,26 @@ func parseConsoleRecord(record ConsoleRecord, secret string) (SemanticEvent, boo
}
return event, true
}
func parseLoginLogRecord(record ConsoleRecord, secret string) (SemanticEvent, bool) {
match := scumLoginLogLine.FindStringSubmatch(record.Text)
if match == nil || !steamID64(match[2]) {
return SemanticEvent{}, false
}
eventType := "scum.login"
if match[4] == "out" {
eventType = "scum.logout"
}
event := SemanticEvent{ServerID: record.ServerID, Sequence: record.Sequence, Type: eventType, PlayerID: match[2], DisplayName: match[3], OccurredAt: record.OccurredAt}
if secret != "" {
event.NetworkCorrelation = networkCorrelation(record.ServerID, match[1], secret)
}
return event, true
}
func networkCorrelation(serverID, value, secret string) string {
digest := sha256.Sum256([]byte(serverID + "\x00" + secret + "\x00" + value))
return hex.EncodeToString(digest[:16])
return hex.EncodeToString(digest[:])
}
func appendDiagnostic(existing []EventDiagnostic, diagnostic EventDiagnostic) []EventDiagnostic {
if len(existing) >= 32 {
@@ -17,4 +17,24 @@ func TestConsoleSemanticEventProducerParsesOnlyBoundedKnownOutput(t *testing.T)
if batch.Events[0].NetworkCorrelation == "10.0.0.1" {
t.Fatal("raw network value leaked")
}
if len(batch.Events[0].NetworkCorrelation) != 64 {
t.Fatalf("network correlation must be full sha256 hex, got %q", batch.Events[0].NetworkCorrelation)
}
}
func TestConsoleSemanticEventProducerParsesScumLoginLog(t *testing.T) {
observedAt := time.Date(2026, 8, 27, 12, 0, 0, 0, time.UTC)
batch := ParseConsoleRecords("server-1", []ConsoleRecord{
{ServerID: "server-1", Stream: "stdout", Sequence: 1, OccurredAt: observedAt, Text: "2026.08.27-12.00.00: '10.0.0.2 76561198000000002:Ada(42)' logged in at: X=1 Y=2 Z=3"},
{ServerID: "server-1", Stream: "stdout", Sequence: 2, OccurredAt: observedAt.Add(time.Second), Text: "2026.08.27-12.00.01: '10.0.0.2 76561198000000002:Ada(42)' logged out"},
}, "fixture-secret")
if len(batch.Events) != 2 || batch.Events[0].Type != "scum.login" || batch.Events[1].Type != "scum.logout" {
t.Fatalf("login log events not parsed: %+v", batch)
}
if batch.Events[0].PlayerID != "76561198000000002" || batch.Events[0].DisplayName != "Ada" || len(batch.Events[0].NetworkCorrelation) != 64 {
t.Fatalf("login event fields are incomplete: %+v", batch.Events[0])
}
if batch.Events[0].NetworkCorrelation == "10.0.0.2" {
t.Fatal("raw login log IP leaked")
}
}