feat(scum): add map trajectory projection

This commit is contained in:
npc0-hue
2026-07-28 17:13:48 +08:00
parent 7f64765c1c
commit 5b15bd50cb
33 changed files with 1073 additions and 49 deletions
@@ -0,0 +1,95 @@
package service
import (
"errors"
"testing"
"time"
"browser.local/platform/domain"
"browser.local/platform/repo"
)
func TestSCUMMapTrajectoryProjectionFiltersAndRetention(t *testing.T) {
svc, runToken := newRegisteredLogIngestService(t)
createLogStreamFixture(t, svc)
enableSCUMMapTrajectory(t, svc)
registered, err := svc.RegisterUser(domain.UserRegistration{DisplayName: "Map Owner", Email: "map-owner@example.test", Password: "secret-password"})
if err != nil {
t.Fatal(err)
}
operator := registered.SessionID
instance, _ := svc.store.ServerInstances().Get("server-1")
instance.OwnerUserID = registered.User.ID
if err := svc.store.ServerInstances().Update(instance); err != nil {
t.Fatal(err)
}
base := time.Date(2026, 7, 28, 12, 0, 0, 0, time.UTC)
entries := []domain.LogEntry{
{Seq: 1, Timestamp: base, Line: "login", Fields: map[string]string{"eventType": "scum.login", "playerId": "steam-map", "playerName": "Moon", "sessionId": "map", "outcome": "accepted"}},
mapEntry(2, base, "player.position", "steam-map", "", "0", "0"), mapEntry(3, base.Add(time.Second), "player.position", "steam-map", "", "1", "1"),
mapEntry(4, base.Add(-time.Minute), "player.position", "steam-map", "", "-100", "-100"), mapEntry(5, base.Add(2*time.Minute), "vehicle.position", "", "jeep-1", "200", "300"),
mapEntry(6, base.Add(3*time.Minute), "player.vehicle.enter", "steam-map", "jeep-1", "", ""), mapEntry(7, base.Add(4*time.Minute), "player.vehicle.enter", "steam-map", "truck-2", "", ""),
}
if _, err := svc.IngestLogBatch(gamePlayerBatch(t, runToken, 1, entries)); err != nil {
t.Fatal(err)
}
players, _ := svc.store.GamePlayers().List(domain.GamePlayerFilter{ServerInstanceID: "server-1"})
if len(players) != 1 {
t.Fatalf("expected player projection, got %+v", players)
}
view, err := svc.GetGameMapTrajectoriesForSession(operator, domain.GameMapTrajectoryQuery{ServerInstanceID: "server-1", From: base.Add(-2 * time.Hour), To: base.Add(time.Hour), PlayerRecordIDs: []string{players[0].ID}, VehicleIDs: []string{"jeep-1"}})
if err != nil {
t.Fatal(err)
}
if view.Status != "ready" || len(view.Players) != 1 || len(view.Players[0].Points) != 2 || view.Players[0].Points[0].MapX >= view.Players[0].Points[1].MapX || view.Players[0].Points[1].MapX != 500 {
t.Fatalf("expected sorted converted and sampled player trail, got %+v", view.Players)
}
if len(view.Vehicles) != 1 || view.Vehicles[0].Points[0].MapX != 700 || len(view.RideSegments) != 1 || view.RideSegments[0].VehicleID != "jeep-1" || view.RideSegments[0].EndedAt.IsZero() {
t.Fatalf("expected filtered vehicle and closed cross-vehicle segment, got vehicles=%+v segments=%+v", view.Vehicles, view.RideSegments)
}
if err := svc.store.GameMapTrackPoints().Create(domain.GameMapTrackPoint{ID: "cross-server", EventID: "cross", ServerInstanceID: "server-2", MapID: "scum-island", MapVersion: "0.9", EntityKind: domain.GameMapTrackEntityVehicle, EntityID: "cross-vehicle", MapX: 1, MapY: 1, OccurredAt: base, CollectedAt: base, ExpiresAt: base.Add(time.Hour)}); err != nil {
t.Fatal(err)
}
isolation, err := svc.GetGameMapTrajectoriesForSession(operator, domain.GameMapTrajectoryQuery{ServerInstanceID: "server-1", From: base.Add(-time.Hour), To: base.Add(time.Hour), VehicleIDs: []string{"cross-vehicle"}})
if err != nil || len(isolation.Vehicles) != 0 {
t.Fatalf("cross-server vehicle leaked: %+v err=%v", isolation, err)
}
if _, err := svc.GetGameMapTrajectoriesForSession("", domain.GameMapTrajectoryQuery{ServerInstanceID: "server-1"}); !errors.Is(err, ErrUnauthorized) {
t.Fatalf("expected unauthorized denial, got %v", err)
}
points, _ := svc.store.GameMapTrackPoints().List(domain.GameMapTrackPointFilter{ServerInstanceID: "server-1"})
points[0].ExpiresAt = base.Add(-time.Second)
if err := svc.store.GameMapTrackPoints().Delete(points[0].ID); err != nil {
t.Fatal(err)
}
if err := svc.store.GameMapTrackPoints().Create(points[0]); err != nil {
t.Fatal(err)
}
svc.now = func() time.Time { return base }
if err := svc.pruneGameMapTrajectories("server-1"); err != nil {
t.Fatal(err)
}
if _, err := svc.store.GameMapTrackPoints().Get(points[0].ID); !errors.Is(err, repo.ErrNotFound) {
t.Fatalf("expired map point retained: %v", err)
}
}
func mapEntry(seq uint64, at time.Time, eventType, playerID, vehicleID, x, y string) domain.LogEntry {
fields := map[string]string{"eventType": eventType, "occurredAt": at.Format(time.RFC3339), "collectedAt": at.Add(time.Second).Format(time.RFC3339), "source": "companion", "mapId": "scum-island", "mapVersion": "0.9", "playerId": playerID, "vehicleId": vehicleID}
if x != "" {
fields["worldX"] = x
fields["worldY"] = y
}
return domain.LogEntry{Seq: seq, Timestamp: at, Line: eventType, Fields: fields}
}
func enableSCUMMapTrajectory(t *testing.T, svc *CoreService) {
t.Helper()
plugin, err := svc.store.GamePlugins().Get("server.scum")
if err != nil {
t.Fatal(err)
}
plugin.MapTrajectories = &domain.GameMapTrajectoryDeclaration{MapID: "scum-island", MapVersion: "0.9", WorldMinX: -500, WorldMinY: -500, WorldMaxX: 500, WorldMaxY: 500, ImageWidth: 2048, ImageHeight: 2048, Precision: 1, SampleDistance: 4, SampleIntervalSeconds: 20, RetentionSeconds: 3600}
if err := svc.store.GamePlugins().Update(plugin); err != nil {
t.Fatal(err)
}
}