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,66 @@
package api
import (
"net/http"
"strings"
"time"
"browser.local/platform/domain"
"browser.local/platform/dto"
)
// serverGameMapTrajectories godoc
// @Summary Read bounded SCUM map trajectories
// @Description Returns only authorized safe map projections, never raw logs, world coordinates, paths, or Companion connection material.
// @Tags game-map-trajectories
// @Produce json
// @Param id path string true "Server instance ID"
// @Param from query string false "RFC3339 window start"
// @Param to query string false "RFC3339 window end"
// @Param playerId query string false "Comma-separated game player record IDs"
// @Param vehicleId query string false "Comma-separated vehicle IDs"
// @Success 200 {object} dto.GameMapTrajectoryResponse
// @Failure 401 {object} dto.ErrorResponse
// @Failure 403 {object} dto.ErrorResponse
// @Failure 400 {object} dto.ErrorResponse
// @Router /api/v1/server-instances/{id}/game-map-trajectories [get]
func (h *coreHandlers) serverGameMapTrajectories(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeMethodNotAllowed(w, http.MethodGet)
return
}
from, err := mapTrajectoryTime(r.URL.Query().Get("from"))
if err != nil {
writeAPIError(w, http.StatusBadRequest, errorCodeBadRequest, "invalid map trajectory from time", nil)
return
}
to, err := mapTrajectoryTime(r.URL.Query().Get("to"))
if err != nil {
writeAPIError(w, http.StatusBadRequest, errorCodeBadRequest, "invalid map trajectory to time", nil)
return
}
value, err := h.core.GetGameMapTrajectoriesForSession(bearerToken(r), domain.GameMapTrajectoryQuery{ServerInstanceID: r.PathValue("id"), From: from, To: to, PlayerRecordIDs: mapTrajectoryIDs(r.URL.Query().Get("playerId")), VehicleIDs: mapTrajectoryIDs(r.URL.Query().Get("vehicleId"))})
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, dto.GameMapTrajectoryFromDomain(value))
}
func mapTrajectoryTime(value string) (time.Time, error) {
if strings.TrimSpace(value) == "" {
return time.Time{}, nil
}
return time.Parse(time.RFC3339, value)
}
func mapTrajectoryIDs(value string) []string {
if strings.TrimSpace(value) == "" {
return nil
}
result := []string{}
for _, item := range strings.Split(value, ",") {
if item = strings.TrimSpace(item); item != "" {
result = append(result, item)
}
}
return result
}
+1
View File
@@ -98,6 +98,7 @@ func (h *coreHandlers) register(mux *http.ServeMux) {
mux.HandleFunc("/api/v1/server-instances/{id}/game-players/{playerId}/state", h.serverGamePlayerState)
mux.HandleFunc("/api/v1/server-instances/{id}/game-players/{playerId}/state-patches", h.serverGamePlayerStatePatches)
mux.HandleFunc("/api/v1/server-instances/{id}/game-players/{playerId}/state-patches/{patchId}/approve", h.serverGamePlayerStatePatchApprove)
mux.HandleFunc("/api/v1/server-instances/{id}/game-map-trajectories", h.serverGameMapTrajectories)
mux.HandleFunc("/api/v1/server-instances/{id}/game-gifts", h.serverGameGiftCatalogs)
mux.HandleFunc("/api/v1/server-instances/{id}/game-gifts/{catalogId}/publish", h.serverGameGiftCatalogPublish)
mux.HandleFunc("/api/v1/server-instances/{id}/game-gifts/{catalogId}/revisions", h.serverGameGiftCatalogRevisions)
+103
View File
@@ -0,0 +1,103 @@
package domain
import "time"
const SCUMMapTrajectoryMapID = "scum-island"
// GameMapTrajectoryDeclaration is plugin-owned metadata used to safely project world coordinates.
type GameMapTrajectoryDeclaration struct {
MapID, MapVersion string
WorldMinX, WorldMinY, WorldMaxX, WorldMaxY float64
ImageWidth, ImageHeight, Precision, SampleDistance float64
SampleIntervalSeconds, RetentionSeconds int
}
type GameMapTrackEntityKind string
const (
GameMapTrackEntityPlayer GameMapTrackEntityKind = "player"
GameMapTrackEntityVehicle GameMapTrackEntityKind = "vehicle"
)
type GameMapTrackPoint struct {
ID, EventID, ServerInstanceID, MapID, MapVersion, EntityID, GamePlayerRecordID, Source string
EntityKind GameMapTrackEntityKind
MapX, MapY float64
OccurredAt, CollectedAt, ExpiresAt time.Time
}
type GameMapTrackPointFilter struct {
ServerInstanceID, MapID, MapVersion, EntityID string
EntityKind GameMapTrackEntityKind
OccurredAfter, OccurredBefore time.Time
Limit int
}
type GamePlayerVehicleSegment struct {
ID, EventID, ServerInstanceID, GamePlayerRecordID, GamePlayerID, VehicleID, MapID, MapVersion string
StartedAt, EndedAt, ExpiresAt time.Time
}
type GamePlayerVehicleSegmentFilter struct {
ServerInstanceID, GamePlayerRecordID, VehicleID, MapID, MapVersion string
OccurredAfter, OccurredBefore time.Time
Limit int
}
type GameMapTrajectoryQuery struct {
ServerInstanceID string
From, To time.Time
PlayerRecordIDs, VehicleIDs []string
}
type GameMapTrajectoryEntity struct {
Kind GameMapTrackEntityKind
EntityID, GamePlayerRecordID, Label string
Points []GameMapTrackPoint
CollectedAt time.Time
Sources []string
}
type GameMapTrajectorySegment struct {
GamePlayerRecordID, VehicleID string
StartedAt, EndedAt time.Time
}
type GameMapTrajectoryView struct {
Status, Reason string
Map GameMapTrajectoryDeclaration
From, To time.Time
Players, Vehicles []GameMapTrajectoryEntity
RideSegments []GameMapTrajectorySegment
}
func CopyGameMapTrajectoryDeclaration(v GameMapTrajectoryDeclaration) GameMapTrajectoryDeclaration {
return v
}
func CopyGameMapTrackPoint(v GameMapTrackPoint) GameMapTrackPoint { return v }
func CopyGameMapTrackPoints(v []GameMapTrackPoint) []GameMapTrackPoint {
out := make([]GameMapTrackPoint, len(v))
copy(out, v)
return out
}
func CopyGamePlayerVehicleSegment(v GamePlayerVehicleSegment) GamePlayerVehicleSegment { return v }
func CopyGamePlayerVehicleSegments(v []GamePlayerVehicleSegment) []GamePlayerVehicleSegment {
out := make([]GamePlayerVehicleSegment, len(v))
copy(out, v)
return out
}
func CopyGameMapTrajectoryQuery(v GameMapTrajectoryQuery) GameMapTrajectoryQuery {
v.PlayerRecordIDs = CopyStringSlice(v.PlayerRecordIDs)
v.VehicleIDs = CopyStringSlice(v.VehicleIDs)
return v
}
func CopyGameMapTrajectoryView(v GameMapTrajectoryView) GameMapTrajectoryView {
v.Map = CopyGameMapTrajectoryDeclaration(v.Map)
v.Players = append([]GameMapTrajectoryEntity(nil), v.Players...)
v.Vehicles = append([]GameMapTrajectoryEntity(nil), v.Vehicles...)
for i := range v.Players {
v.Players[i].Points = CopyGameMapTrackPoints(v.Players[i].Points)
v.Players[i].Sources = CopyStringSlice(v.Players[i].Sources)
}
for i := range v.Vehicles {
v.Vehicles[i].Points = CopyGameMapTrackPoints(v.Vehicles[i].Points)
v.Vehicles[i].Sources = CopyStringSlice(v.Vehicles[i].Sources)
}
v.RideSegments = append([]GameMapTrajectorySegment(nil), v.RideSegments...)
return v
}
+15
View File
@@ -620,6 +620,7 @@ type GamePluginManifest struct {
RemoteAccess GamePluginRemoteAccess
RuntimeProfiles GamePluginRuntimeProfiles
GameClientBridge GameClientBridgeManifest
MapTrajectories *GameMapTrajectoryDeclaration
}
type GamePluginManifestRegistration struct {
@@ -651,6 +652,7 @@ type GamePlugin struct {
RemoteAccess GamePluginRemoteAccess
RuntimeProfiles GamePluginRuntimeProfiles
GameClientBridge GameClientBridgeManifest
MapTrajectories *GameMapTrajectoryDeclaration
ValidationViolations []string
Status GamePluginStatus
}
@@ -678,6 +680,7 @@ type PluginMarketplacePlugin struct {
RemoteAccess GamePluginRemoteAccess
RuntimeProfiles GamePluginRuntimeProfiles
GameClientBridge GameClientBridgeManifest
MapTrajectories *GameMapTrajectoryDeclaration
ValidationViolations []string
Status GamePluginStatus
Source string
@@ -1639,6 +1642,10 @@ func CopyGamePlugin(plugin GamePlugin) GamePlugin {
plugin.RemoteAccess = CopyGamePluginRemoteAccess(plugin.RemoteAccess)
plugin.RuntimeProfiles = CopyGamePluginRuntimeProfiles(plugin.RuntimeProfiles)
plugin.GameClientBridge = CopyGameClientBridgeManifest(plugin.GameClientBridge)
if plugin.MapTrajectories != nil {
value := CopyGameMapTrajectoryDeclaration(*plugin.MapTrajectories)
plugin.MapTrajectories = &value
}
plugin.ValidationViolations = CopyStringSlice(plugin.ValidationViolations)
return plugin
}
@@ -1656,6 +1663,10 @@ func CopyPluginMarketplacePlugin(plugin PluginMarketplacePlugin) PluginMarketpla
plugin.RemoteAccess = CopyGamePluginRemoteAccess(plugin.RemoteAccess)
plugin.RuntimeProfiles = CopyGamePluginRuntimeProfiles(plugin.RuntimeProfiles)
plugin.GameClientBridge = CopyGameClientBridgeManifest(plugin.GameClientBridge)
if plugin.MapTrajectories != nil {
value := CopyGameMapTrajectoryDeclaration(*plugin.MapTrajectories)
plugin.MapTrajectories = &value
}
plugin.ValidationViolations = CopyStringSlice(plugin.ValidationViolations)
return plugin
}
@@ -1690,6 +1701,10 @@ func CopyGamePluginManifest(manifest GamePluginManifest) GamePluginManifest {
manifest.RemoteAccess = CopyGamePluginRemoteAccess(manifest.RemoteAccess)
manifest.RuntimeProfiles = CopyGamePluginRuntimeProfiles(manifest.RuntimeProfiles)
manifest.GameClientBridge = CopyGameClientBridgeManifest(manifest.GameClientBridge)
if manifest.MapTrajectories != nil {
value := CopyGameMapTrajectoryDeclaration(*manifest.MapTrajectories)
manifest.MapTrajectories = &value
}
return manifest
}
+69
View File
@@ -0,0 +1,69 @@
package dto
import (
"browser.local/platform/domain"
"time"
)
type GameMapTrajectoryPointResponse struct {
MapX float64 `json:"mapX"`
MapY float64 `json:"mapY"`
OccurredAt time.Time `json:"occurredAt"`
CollectedAt time.Time `json:"collectedAt"`
Source string `json:"source"`
}
type GameMapTrajectoryEntityResponse struct {
Kind string `json:"kind"`
EntityID string `json:"entityId"`
GamePlayerRecordID string `json:"gamePlayerRecordId,omitempty"`
Label string `json:"label"`
Points []GameMapTrajectoryPointResponse `json:"points"`
CollectedAt time.Time `json:"collectedAt,omitempty"`
Sources []string `json:"sources"`
}
type GameMapTrajectorySegmentResponse struct {
GamePlayerRecordID string `json:"gamePlayerRecordId"`
VehicleID string `json:"vehicleId"`
StartedAt time.Time `json:"startedAt"`
EndedAt time.Time `json:"endedAt,omitempty"`
}
type GameMapTrajectoryMapResponse struct {
MapID string `json:"mapId"`
MapVersion string `json:"mapVersion"`
ImageWidth float64 `json:"imageWidth"`
ImageHeight float64 `json:"imageHeight"`
Precision float64 `json:"precision"`
}
type GameMapTrajectoryResponse struct {
Status string `json:"status"`
Reason string `json:"reason,omitempty"`
Map *GameMapTrajectoryMapResponse `json:"map,omitempty"`
From time.Time `json:"from,omitempty"`
To time.Time `json:"to,omitempty"`
Players []GameMapTrajectoryEntityResponse `json:"players"`
Vehicles []GameMapTrajectoryEntityResponse `json:"vehicles"`
RideSegments []GameMapTrajectorySegmentResponse `json:"rideSegments"`
}
func GameMapTrajectoryFromDomain(value domain.GameMapTrajectoryView) GameMapTrajectoryResponse {
value = domain.CopyGameMapTrajectoryView(value)
response := GameMapTrajectoryResponse{Status: value.Status, Reason: value.Reason, From: value.From, To: value.To, Players: mapTrajectoryEntitiesFromDomain(value.Players), Vehicles: mapTrajectoryEntitiesFromDomain(value.Vehicles), RideSegments: make([]GameMapTrajectorySegmentResponse, len(value.RideSegments))}
if value.Status != "missing-map" {
response.Map = &GameMapTrajectoryMapResponse{MapID: value.Map.MapID, MapVersion: value.Map.MapVersion, ImageWidth: value.Map.ImageWidth, ImageHeight: value.Map.ImageHeight, Precision: value.Map.Precision}
}
for i, segment := range value.RideSegments {
response.RideSegments[i] = GameMapTrajectorySegmentResponse{GamePlayerRecordID: segment.GamePlayerRecordID, VehicleID: segment.VehicleID, StartedAt: segment.StartedAt, EndedAt: segment.EndedAt}
}
return response
}
func mapTrajectoryEntitiesFromDomain(values []domain.GameMapTrajectoryEntity) []GameMapTrajectoryEntityResponse {
result := make([]GameMapTrajectoryEntityResponse, len(values))
for i, value := range values {
points := make([]GameMapTrajectoryPointResponse, len(value.Points))
for j, point := range value.Points {
points[j] = GameMapTrajectoryPointResponse{MapX: point.MapX, MapY: point.MapY, OccurredAt: point.OccurredAt, CollectedAt: point.CollectedAt, Source: point.Source}
}
result[i] = GameMapTrajectoryEntityResponse{Kind: string(value.Kind), EntityID: value.EntityID, GamePlayerRecordID: value.GamePlayerRecordID, Label: value.Label, Points: points, CollectedAt: value.CollectedAt, Sources: domain.CopyStringSlice(value.Sources)}
}
return result
}
+36
View File
@@ -321,6 +321,20 @@ type GameClientBridgeManifestBody struct {
Pages []GameClientBridgePageContractBody `json:"pages,omitempty"`
Companion *GameClientBridgeCompanionDeclarationBody `json:"companion,omitempty"`
}
type GameMapTrajectoryDeclarationBody struct {
MapID string `json:"mapId"`
MapVersion string `json:"mapVersion"`
WorldMinX float64 `json:"worldMinX"`
WorldMinY float64 `json:"worldMinY"`
WorldMaxX float64 `json:"worldMaxX"`
WorldMaxY float64 `json:"worldMaxY"`
ImageWidth float64 `json:"imageWidth"`
ImageHeight float64 `json:"imageHeight"`
Precision float64 `json:"precision"`
SampleDistance float64 `json:"sampleDistance"`
SampleIntervalSeconds int `json:"sampleIntervalSeconds"`
RetentionSeconds int `json:"retentionSeconds"`
}
type GamePluginManifestBody struct {
ID string `json:"id"`
@@ -341,6 +355,7 @@ type GamePluginManifestBody struct {
RemoteAccess GamePluginRemoteAccessBody `json:"remoteAccess,omitempty"`
RuntimeProfiles GamePluginRuntimeProfilesBody `json:"runtimeProfiles,omitempty"`
GameClientBridge GameClientBridgeManifestBody `json:"gameClientBridge,omitempty"`
MapTrajectories *GameMapTrajectoryDeclarationBody `json:"mapTrajectories,omitempty"`
}
type GamePluginManifestRegistrationRequest struct {
@@ -372,6 +387,7 @@ type GamePluginCreateRequest struct {
RemoteAccess GamePluginRemoteAccessBody `json:"remoteAccess,omitempty"`
RuntimeProfiles GamePluginRuntimeProfilesBody `json:"runtimeProfiles,omitempty"`
GameClientBridge GameClientBridgeManifestBody `json:"gameClientBridge,omitempty"`
MapTrajectories *GameMapTrajectoryDeclarationBody `json:"mapTrajectories,omitempty"`
ValidationViolations []string `json:"validationViolations,omitempty"`
}
@@ -399,6 +415,7 @@ type GamePluginResponse struct {
RemoteAccess GamePluginRemoteAccessBody `json:"remoteAccess,omitempty"`
RuntimeProfiles GamePluginRuntimeProfilesResponseBody `json:"runtimeProfiles,omitempty"`
GameClientBridge GameClientBridgeManifestBody `json:"gameClientBridge,omitempty"`
MapTrajectories *GameMapTrajectoryDeclarationBody `json:"mapTrajectories,omitempty"`
ValidationViolations []string `json:"validationViolations,omitempty"`
Status domain.GamePluginStatus `json:"status"`
}
@@ -430,6 +447,7 @@ type MarketplacePluginResponse struct {
RemoteAccess GamePluginRemoteAccessBody `json:"remoteAccess,omitempty"`
RuntimeProfiles GamePluginRuntimeProfilesResponseBody `json:"runtimeProfiles,omitempty"`
GameClientBridge GameClientBridgeManifestBody `json:"gameClientBridge,omitempty"`
MapTrajectories *GameMapTrajectoryDeclarationBody `json:"mapTrajectories,omitempty"`
ValidationViolations []string `json:"validationViolations,omitempty"`
Status domain.GamePluginStatus `json:"status"`
Source string `json:"source"`
@@ -974,10 +992,25 @@ func (request GamePluginManifestRegistrationRequest) ToDomain() domain.GamePlugi
RemoteAccess: request.Manifest.RemoteAccess.ToDomain(),
RuntimeProfiles: request.Manifest.RuntimeProfiles.ToDomain(),
GameClientBridge: request.Manifest.GameClientBridge.ToDomain(),
MapTrajectories: mapTrajectoryDeclarationToDomain(request.Manifest.MapTrajectories),
},
}
}
func mapTrajectoryDeclarationToDomain(value *GameMapTrajectoryDeclarationBody) *domain.GameMapTrajectoryDeclaration {
if value == nil {
return nil
}
result := domain.GameMapTrajectoryDeclaration{MapID: value.MapID, MapVersion: value.MapVersion, WorldMinX: value.WorldMinX, WorldMinY: value.WorldMinY, WorldMaxX: value.WorldMaxX, WorldMaxY: value.WorldMaxY, ImageWidth: value.ImageWidth, ImageHeight: value.ImageHeight, Precision: value.Precision, SampleDistance: value.SampleDistance, SampleIntervalSeconds: value.SampleIntervalSeconds, RetentionSeconds: value.RetentionSeconds}
return &result
}
func mapTrajectoryDeclarationFromDomain(value *domain.GameMapTrajectoryDeclaration) *GameMapTrajectoryDeclarationBody {
if value == nil {
return nil
}
return &GameMapTrajectoryDeclarationBody{MapID: value.MapID, MapVersion: value.MapVersion, WorldMinX: value.WorldMinX, WorldMinY: value.WorldMinY, WorldMaxX: value.WorldMaxX, WorldMaxY: value.WorldMaxY, ImageWidth: value.ImageWidth, ImageHeight: value.ImageHeight, Precision: value.Precision, SampleDistance: value.SampleDistance, SampleIntervalSeconds: value.SampleIntervalSeconds, RetentionSeconds: value.RetentionSeconds}
}
func fileWorkspaceToDomain(body PluginFileWorkspaceBody) domain.PluginFileWorkspace {
workspace := domain.PluginFileWorkspace{DefaultDirectoryKey: body.DefaultDirectoryKey}
for _, item := range body.Directories {
@@ -1119,6 +1152,7 @@ func (request GamePluginCreateRequest) ToDomain() domain.GamePlugin {
RemoteAccess: request.RemoteAccess.ToDomain(),
RuntimeProfiles: request.RuntimeProfiles.ToDomain(),
GameClientBridge: request.GameClientBridge.ToDomain(),
MapTrajectories: mapTrajectoryDeclarationToDomain(request.MapTrajectories),
ValidationViolations: domain.CopyStringSlice(request.ValidationViolations),
}
}
@@ -1367,6 +1401,7 @@ func GamePluginFromDomain(plugin domain.GamePlugin) GamePluginResponse {
RemoteAccess: remoteAccessFromDomain(plugin.RemoteAccess),
RuntimeProfiles: runtimeProfilesFromDomain(plugin.RuntimeProfiles),
GameClientBridge: gameClientBridgeManifestFromDomain(plugin.GameClientBridge),
MapTrajectories: mapTrajectoryDeclarationFromDomain(plugin.MapTrajectories),
ValidationViolations: plugin.ValidationViolations,
Status: plugin.Status,
}
@@ -1458,6 +1493,7 @@ func MarketplacePluginFromDomain(plugin domain.PluginMarketplacePlugin) Marketpl
RemoteAccess: remoteAccessFromDomain(plugin.RemoteAccess),
RuntimeProfiles: runtimeProfilesFromDomain(plugin.RuntimeProfiles),
GameClientBridge: gameClientBridgeManifestFromDomain(plugin.GameClientBridge),
MapTrajectories: mapTrajectoryDeclarationFromDomain(plugin.MapTrajectories),
ValidationViolations: plugin.ValidationViolations,
Status: plugin.Status,
Source: plugin.Source,
+40
View File
@@ -0,0 +1,40 @@
package model
import "time"
// GameMapTrackPoint is the safe, map-normalized and retention-bounded trajectory table shape.
type GameMapTrackPoint struct {
ID string `json:"id" db:"id"`
EventID string `json:"eventId" db:"event_id"`
ServerInstanceID string `json:"serverInstanceId" db:"server_instance_id"`
MapID string `json:"mapId" db:"map_id"`
MapVersion string `json:"mapVersion" db:"map_version"`
EntityKind string `json:"entityKind" db:"entity_kind"`
EntityID string `json:"entityId" db:"entity_id"`
GamePlayerRecordID string `json:"gamePlayerRecordId,omitempty" db:"game_player_record_id"`
MapX float64 `json:"mapX" db:"map_x"`
MapY float64 `json:"mapY" db:"map_y"`
Source string `json:"source" db:"source"`
OccurredAt time.Time `json:"occurredAt" db:"occurred_at"`
CollectedAt time.Time `json:"collectedAt" db:"collected_at"`
ExpiresAt time.Time `json:"expiresAt" db:"expires_at"`
}
func (GameMapTrackPoint) TableName() string { return "game_map_track_points" }
// GamePlayerVehicleSegment is a server-local, typed ride association; it contains no vehicle storage details.
type GamePlayerVehicleSegment struct {
ID string `json:"id" db:"id"`
EventID string `json:"eventId" db:"event_id"`
ServerInstanceID string `json:"serverInstanceId" db:"server_instance_id"`
GamePlayerRecordID string `json:"gamePlayerRecordId" db:"game_player_record_id"`
GamePlayerID string `json:"gamePlayerId" db:"game_player_id"`
VehicleID string `json:"vehicleId" db:"vehicle_id"`
MapID string `json:"mapId" db:"map_id"`
MapVersion string `json:"mapVersion" db:"map_version"`
StartedAt time.Time `json:"startedAt" db:"started_at"`
EndedAt time.Time `json:"endedAt,omitempty" db:"ended_at"`
ExpiresAt time.Time `json:"expiresAt" db:"expires_at"`
}
func (GamePlayerVehicleSegment) TableName() string { return "game_player_vehicle_segments" }
+11 -1
View File
@@ -48,6 +48,8 @@ type StoreSnapshot struct {
GameAccessAttempts []domain.GameAccessAttempt `json:"gameAccessAttempts"`
GameSecuritySignals []domain.GameSecuritySignal `json:"gameSecuritySignals"`
GamePlayerStatePatches []domain.GamePlayerStatePatch `json:"gamePlayerStatePatches"`
GameMapTrackPoints []domain.GameMapTrackPoint `json:"gameMapTrackPoints"`
GamePlayerVehicleSegments []domain.GamePlayerVehicleSegment `json:"gamePlayerVehicleSegments"`
GameGiftCatalogs []domain.GameGiftCatalog `json:"gameGiftCatalogs"`
GameGiftRevisions []domain.GameGiftRevision `json:"gameGiftRevisions"`
GameGiftGrants []domain.GameGiftGrant `json:"gameGiftGrants"`
@@ -217,6 +219,12 @@ func (store *FileStore) GameSecuritySignals() GameSecuritySignalRepository {
func (store *FileStore) GamePlayerStatePatches() GamePlayerStatePatchRepository {
return &persistentRepository[domain.GamePlayerStatePatch, domain.GamePlayerStatePatchFilter]{repository: store.MemoryStore.gamePlayerStatePatches, persist: store.persist}
}
func (store *FileStore) GameMapTrackPoints() GameMapTrackPointRepository {
return &persistentRepository[domain.GameMapTrackPoint, domain.GameMapTrackPointFilter]{repository: store.MemoryStore.gameMapTrackPoints, persist: store.persist}
}
func (store *FileStore) GamePlayerVehicleSegments() GamePlayerVehicleSegmentRepository {
return &persistentRepository[domain.GamePlayerVehicleSegment, domain.GamePlayerVehicleSegmentFilter]{repository: store.MemoryStore.gamePlayerVehicleSegments, persist: store.persist}
}
func (store *FileStore) GameGiftCatalogs() GameGiftCatalogRepository {
return &persistentRepository[domain.GameGiftCatalog, domain.GameGiftCatalogFilter]{repository: store.MemoryStore.gameGiftCatalogs, persist: store.persist}
}
@@ -299,7 +307,7 @@ func (store *FileStore) snapshot() StoreSnapshot {
GameClientBridgeCommands: snapshotRepository(store.MemoryStore.bridgeCommands.memoryRepository),
GameClientBridgeSnapshots: snapshotRepository(store.MemoryStore.bridgeSnapshots.memoryRepository),
GameClientBridgeStreams: snapshotRepository(store.MemoryStore.bridgeStreams),
GamePlayers: snapshotRepository(store.MemoryStore.gamePlayers), GamePlayerAliases: snapshotRepository(store.MemoryStore.gamePlayerAliases), GamePlayerSessions: snapshotRepository(store.MemoryStore.gamePlayerSessions), GameAccessAttempts: snapshotRepository(store.MemoryStore.gameAccessAttempts), GameSecuritySignals: snapshotRepository(store.MemoryStore.gameSecuritySignals), GamePlayerStatePatches: snapshotRepository(store.MemoryStore.gamePlayerStatePatches), GameGiftCatalogs: snapshotRepository(store.MemoryStore.gameGiftCatalogs), GameGiftRevisions: snapshotRepository(store.MemoryStore.gameGiftRevisions), GameGiftGrants: snapshotRepository(store.MemoryStore.gameGiftGrants),
GamePlayers: snapshotRepository(store.MemoryStore.gamePlayers), GamePlayerAliases: snapshotRepository(store.MemoryStore.gamePlayerAliases), GamePlayerSessions: snapshotRepository(store.MemoryStore.gamePlayerSessions), GameAccessAttempts: snapshotRepository(store.MemoryStore.gameAccessAttempts), GameSecuritySignals: snapshotRepository(store.MemoryStore.gameSecuritySignals), GamePlayerStatePatches: snapshotRepository(store.MemoryStore.gamePlayerStatePatches), GameMapTrackPoints: snapshotRepository(store.MemoryStore.gameMapTrackPoints), GamePlayerVehicleSegments: snapshotRepository(store.MemoryStore.gamePlayerVehicleSegments), GameGiftCatalogs: snapshotRepository(store.MemoryStore.gameGiftCatalogs), GameGiftRevisions: snapshotRepository(store.MemoryStore.gameGiftRevisions), GameGiftGrants: snapshotRepository(store.MemoryStore.gameGiftGrants),
}
}
@@ -339,6 +347,8 @@ func (store *FileStore) loadSnapshot(snapshot StoreSnapshot) {
loadRepository(store.MemoryStore.gameAccessAttempts, snapshot.GameAccessAttempts)
loadRepository(store.MemoryStore.gameSecuritySignals, snapshot.GameSecuritySignals)
loadRepository(store.MemoryStore.gamePlayerStatePatches, snapshot.GamePlayerStatePatches)
loadRepository(store.MemoryStore.gameMapTrackPoints, snapshot.GameMapTrackPoints)
loadRepository(store.MemoryStore.gamePlayerVehicleSegments, snapshot.GamePlayerVehicleSegments)
loadRepository(store.MemoryStore.gameGiftCatalogs, snapshot.GameGiftCatalogs)
loadRepository(store.MemoryStore.gameGiftRevisions, snapshot.GameGiftRevisions)
loadRepository(store.MemoryStore.gameGiftGrants, snapshot.GameGiftGrants)
+9 -1
View File
@@ -189,6 +189,12 @@ func (store *MySQLStore) GameSecuritySignals() GameSecuritySignalRepository {
func (store *MySQLStore) GamePlayerStatePatches() GamePlayerStatePatchRepository {
return &persistentRepository[domain.GamePlayerStatePatch, domain.GamePlayerStatePatchFilter]{repository: store.MemoryStore.gamePlayerStatePatches, persist: store.persist}
}
func (store *MySQLStore) GameMapTrackPoints() GameMapTrackPointRepository {
return &persistentRepository[domain.GameMapTrackPoint, domain.GameMapTrackPointFilter]{repository: store.MemoryStore.gameMapTrackPoints, persist: store.persist}
}
func (store *MySQLStore) GamePlayerVehicleSegments() GamePlayerVehicleSegmentRepository {
return &persistentRepository[domain.GamePlayerVehicleSegment, domain.GamePlayerVehicleSegmentFilter]{repository: store.MemoryStore.gamePlayerVehicleSegments, persist: store.persist}
}
func (store *MySQLStore) GameGiftCatalogs() GameGiftCatalogRepository {
return &persistentRepository[domain.GameGiftCatalog, domain.GameGiftCatalogFilter]{repository: store.MemoryStore.gameGiftCatalogs, persist: store.persist}
}
@@ -288,7 +294,7 @@ func (store *MySQLStore) snapshot() StoreSnapshot {
GameClientBridgeCommands: snapshotRepository(store.MemoryStore.bridgeCommands.memoryRepository),
GameClientBridgeSnapshots: snapshotRepository(store.MemoryStore.bridgeSnapshots.memoryRepository),
GameClientBridgeStreams: snapshotRepository(store.MemoryStore.bridgeStreams),
GamePlayers: snapshotRepository(store.MemoryStore.gamePlayers), GamePlayerAliases: snapshotRepository(store.MemoryStore.gamePlayerAliases), GamePlayerSessions: snapshotRepository(store.MemoryStore.gamePlayerSessions), GameAccessAttempts: snapshotRepository(store.MemoryStore.gameAccessAttempts), GameSecuritySignals: snapshotRepository(store.MemoryStore.gameSecuritySignals), GamePlayerStatePatches: snapshotRepository(store.MemoryStore.gamePlayerStatePatches), GameGiftCatalogs: snapshotRepository(store.MemoryStore.gameGiftCatalogs), GameGiftRevisions: snapshotRepository(store.MemoryStore.gameGiftRevisions), GameGiftGrants: snapshotRepository(store.MemoryStore.gameGiftGrants),
GamePlayers: snapshotRepository(store.MemoryStore.gamePlayers), GamePlayerAliases: snapshotRepository(store.MemoryStore.gamePlayerAliases), GamePlayerSessions: snapshotRepository(store.MemoryStore.gamePlayerSessions), GameAccessAttempts: snapshotRepository(store.MemoryStore.gameAccessAttempts), GameSecuritySignals: snapshotRepository(store.MemoryStore.gameSecuritySignals), GamePlayerStatePatches: snapshotRepository(store.MemoryStore.gamePlayerStatePatches), GameMapTrackPoints: snapshotRepository(store.MemoryStore.gameMapTrackPoints), GamePlayerVehicleSegments: snapshotRepository(store.MemoryStore.gamePlayerVehicleSegments), GameGiftCatalogs: snapshotRepository(store.MemoryStore.gameGiftCatalogs), GameGiftRevisions: snapshotRepository(store.MemoryStore.gameGiftRevisions), GameGiftGrants: snapshotRepository(store.MemoryStore.gameGiftGrants),
}
}
@@ -328,6 +334,8 @@ func (store *MySQLStore) loadSnapshot(snapshot StoreSnapshot) {
loadRepository(store.MemoryStore.gameAccessAttempts, snapshot.GameAccessAttempts)
loadRepository(store.MemoryStore.gameSecuritySignals, snapshot.GameSecuritySignals)
loadRepository(store.MemoryStore.gamePlayerStatePatches, snapshot.GamePlayerStatePatches)
loadRepository(store.MemoryStore.gameMapTrackPoints, snapshot.GameMapTrackPoints)
loadRepository(store.MemoryStore.gamePlayerVehicleSegments, snapshot.GamePlayerVehicleSegments)
loadRepository(store.MemoryStore.gameGiftCatalogs, snapshot.GameGiftCatalogs)
loadRepository(store.MemoryStore.gameGiftRevisions, snapshot.GameGiftRevisions)
loadRepository(store.MemoryStore.gameGiftGrants, snapshot.GameGiftGrants)
+78 -47
View File
@@ -265,6 +265,19 @@ type GamePlayerStatePatchRepository interface {
List(domain.GamePlayerStatePatchFilter) ([]domain.GamePlayerStatePatch, error)
Update(domain.GamePlayerStatePatch) error
}
type GameMapTrackPointRepository interface {
Create(domain.GameMapTrackPoint) error
Get(string) (domain.GameMapTrackPoint, error)
List(domain.GameMapTrackPointFilter) ([]domain.GameMapTrackPoint, error)
Delete(string) error
}
type GamePlayerVehicleSegmentRepository interface {
Create(domain.GamePlayerVehicleSegment) error
Get(string) (domain.GamePlayerVehicleSegment, error)
List(domain.GamePlayerVehicleSegmentFilter) ([]domain.GamePlayerVehicleSegment, error)
Update(domain.GamePlayerVehicleSegment) error
Delete(string) error
}
type GameGiftCatalogRepository interface {
Create(domain.GameGiftCatalog) error
Get(string) (domain.GameGiftCatalog, error)
@@ -319,50 +332,54 @@ type Store interface {
GameAccessAttempts() GameAccessAttemptRepository
GameSecuritySignals() GameSecuritySignalRepository
GamePlayerStatePatches() GamePlayerStatePatchRepository
GameMapTrackPoints() GameMapTrackPointRepository
GamePlayerVehicleSegments() GamePlayerVehicleSegmentRepository
GameGiftCatalogs() GameGiftCatalogRepository
GameGiftRevisions() GameGiftRevisionRepository
GameGiftGrants() GameGiftGrantRepository
}
type MemoryStore struct {
users *memoryRepository[domain.User, domain.UserFilter]
authSessions *memoryRepository[domain.AuthSessionRecord, domain.AuthSessionFilter]
runSessions *memoryRepository[domain.RunControlSession, struct{}]
aiProviders *memoryRepository[domain.AIProvider, domain.AIProviderFilter]
gamePlugins *memoryRepository[domain.GamePlugin, domain.GamePluginFilter]
serverInstances *memoryRepository[domain.ServerInstance, domain.ServerInstanceFilter]
runEndpoints *memoryRepository[domain.RunEndpoint, domain.RunEndpointFilter]
jobs *memoryJobRepository
artifacts *memoryRepository[domain.Artifact, domain.ArtifactFilter]
runtimeBindings *memoryRepository[domain.RuntimeBinding, domain.RuntimeBindingFilter]
componentKeys *memoryRepository[domain.EncryptedComponentKey, domain.EncryptedComponentKeyFilter]
runDists *memoryRepository[domain.RunDistribution, domain.RunDistributionFilter]
clientDists *memoryRepository[domain.ClientManagerDistribution, domain.ClientManagerDistributionFilter]
clientInstalls *memoryRepository[domain.ClientManagerInstallation, domain.ClientManagerInstallationFilter]
clientSessions *memoryRepository[domain.ClientManagerSession, domain.ClientManagerSessionFilter]
clientNonces *memoryRepository[domain.ClientManagerRegistrationNonce, domain.ClientManagerNonceFilter]
dependencies *memoryRepository[domain.DependencyStatus, domain.DependencyStatusFilter]
buildJobs *memoryRepository[domain.ClientManagerBuildJob, domain.ClientManagerBuildJobFilter]
updateJobs *memoryRepository[domain.RunUpdateJob, domain.RunUpdateJobFilter]
logStreams *memoryRepository[domain.LogStream, domain.LogStreamFilter]
auditEvents *memoryRepository[domain.AuditEvent, domain.AuditEventFilter]
metricSamples *memoryRepository[domain.MetricSample, domain.MetricSampleFilter]
backups *memoryRepository[domain.BackupRecord, domain.BackupFilter]
alerts *memoryRepository[domain.AlertRecord, domain.AlertFilter]
pluginLifecycle *memoryRepository[domain.PluginLifecycleInstallation, domain.PluginLifecycleFilter]
aiConfigDiffs *memoryRepository[domain.AIConfigDiffPreview, domain.AIConfigDiffFilter]
bridgeCommands *memoryGameClientBridgeCommandRepository
bridgeSnapshots *memoryGameClientBridgeSnapshotRepository
bridgeStreams *memoryRepository[domain.GameClientBridgeSnapshotStream, domain.GameClientBridgeSnapshotStreamFilter]
gamePlayers *memoryRepository[domain.GamePlayer, domain.GamePlayerFilter]
gamePlayerAliases *memoryRepository[domain.GamePlayerAlias, domain.GamePlayerAliasFilter]
gamePlayerSessions *memoryRepository[domain.GamePlayerSession, domain.GamePlayerSessionFilter]
gameAccessAttempts *memoryRepository[domain.GameAccessAttempt, domain.GameAccessAttemptFilter]
gameSecuritySignals *memoryRepository[domain.GameSecuritySignal, domain.GameSecuritySignalFilter]
gamePlayerStatePatches *memoryRepository[domain.GamePlayerStatePatch, domain.GamePlayerStatePatchFilter]
gameGiftCatalogs *memoryRepository[domain.GameGiftCatalog, domain.GameGiftCatalogFilter]
gameGiftRevisions *memoryRepository[domain.GameGiftRevision, domain.GameGiftRevisionFilter]
gameGiftGrants *memoryRepository[domain.GameGiftGrant, domain.GameGiftGrantFilter]
users *memoryRepository[domain.User, domain.UserFilter]
authSessions *memoryRepository[domain.AuthSessionRecord, domain.AuthSessionFilter]
runSessions *memoryRepository[domain.RunControlSession, struct{}]
aiProviders *memoryRepository[domain.AIProvider, domain.AIProviderFilter]
gamePlugins *memoryRepository[domain.GamePlugin, domain.GamePluginFilter]
serverInstances *memoryRepository[domain.ServerInstance, domain.ServerInstanceFilter]
runEndpoints *memoryRepository[domain.RunEndpoint, domain.RunEndpointFilter]
jobs *memoryJobRepository
artifacts *memoryRepository[domain.Artifact, domain.ArtifactFilter]
runtimeBindings *memoryRepository[domain.RuntimeBinding, domain.RuntimeBindingFilter]
componentKeys *memoryRepository[domain.EncryptedComponentKey, domain.EncryptedComponentKeyFilter]
runDists *memoryRepository[domain.RunDistribution, domain.RunDistributionFilter]
clientDists *memoryRepository[domain.ClientManagerDistribution, domain.ClientManagerDistributionFilter]
clientInstalls *memoryRepository[domain.ClientManagerInstallation, domain.ClientManagerInstallationFilter]
clientSessions *memoryRepository[domain.ClientManagerSession, domain.ClientManagerSessionFilter]
clientNonces *memoryRepository[domain.ClientManagerRegistrationNonce, domain.ClientManagerNonceFilter]
dependencies *memoryRepository[domain.DependencyStatus, domain.DependencyStatusFilter]
buildJobs *memoryRepository[domain.ClientManagerBuildJob, domain.ClientManagerBuildJobFilter]
updateJobs *memoryRepository[domain.RunUpdateJob, domain.RunUpdateJobFilter]
logStreams *memoryRepository[domain.LogStream, domain.LogStreamFilter]
auditEvents *memoryRepository[domain.AuditEvent, domain.AuditEventFilter]
metricSamples *memoryRepository[domain.MetricSample, domain.MetricSampleFilter]
backups *memoryRepository[domain.BackupRecord, domain.BackupFilter]
alerts *memoryRepository[domain.AlertRecord, domain.AlertFilter]
pluginLifecycle *memoryRepository[domain.PluginLifecycleInstallation, domain.PluginLifecycleFilter]
aiConfigDiffs *memoryRepository[domain.AIConfigDiffPreview, domain.AIConfigDiffFilter]
bridgeCommands *memoryGameClientBridgeCommandRepository
bridgeSnapshots *memoryGameClientBridgeSnapshotRepository
bridgeStreams *memoryRepository[domain.GameClientBridgeSnapshotStream, domain.GameClientBridgeSnapshotStreamFilter]
gamePlayers *memoryRepository[domain.GamePlayer, domain.GamePlayerFilter]
gamePlayerAliases *memoryRepository[domain.GamePlayerAlias, domain.GamePlayerAliasFilter]
gamePlayerSessions *memoryRepository[domain.GamePlayerSession, domain.GamePlayerSessionFilter]
gameAccessAttempts *memoryRepository[domain.GameAccessAttempt, domain.GameAccessAttemptFilter]
gameSecuritySignals *memoryRepository[domain.GameSecuritySignal, domain.GameSecuritySignalFilter]
gamePlayerStatePatches *memoryRepository[domain.GamePlayerStatePatch, domain.GamePlayerStatePatchFilter]
gameMapTrackPoints *memoryRepository[domain.GameMapTrackPoint, domain.GameMapTrackPointFilter]
gamePlayerVehicleSegments *memoryRepository[domain.GamePlayerVehicleSegment, domain.GamePlayerVehicleSegmentFilter]
gameGiftCatalogs *memoryRepository[domain.GameGiftCatalog, domain.GameGiftCatalogFilter]
gameGiftRevisions *memoryRepository[domain.GameGiftRevision, domain.GameGiftRevisionFilter]
gameGiftGrants *memoryRepository[domain.GameGiftGrant, domain.GameGiftGrantFilter]
}
func NewMemoryStore() *MemoryStore {
@@ -500,15 +517,17 @@ func NewMemoryStore() *MemoryStore {
domain.CopyGameClientBridgeSnapshotStream,
matchGameClientBridgeSnapshotStream,
),
gamePlayers: newMemoryRepository(func(v domain.GamePlayer) string { return v.ID }, domain.CopyGamePlayer, matchGamePlayer),
gamePlayerAliases: newMemoryRepository(func(v domain.GamePlayerAlias) string { return v.ID }, domain.CopyGamePlayerAlias, matchGamePlayerAlias),
gamePlayerSessions: newMemoryRepository(func(v domain.GamePlayerSession) string { return v.ID }, domain.CopyGamePlayerSession, matchGamePlayerSession),
gameAccessAttempts: newMemoryRepository(func(v domain.GameAccessAttempt) string { return v.ID }, domain.CopyGameAccessAttempt, matchGameAccessAttempt),
gameSecuritySignals: newMemoryRepository(func(v domain.GameSecuritySignal) string { return v.ID }, domain.CopyGameSecuritySignal, matchGameSecuritySignal),
gamePlayerStatePatches: newMemoryRepository(func(v domain.GamePlayerStatePatch) string { return v.ID }, domain.CopyGamePlayerStatePatch, matchGamePlayerStatePatch),
gameGiftCatalogs: newMemoryRepository(func(v domain.GameGiftCatalog) string { return v.ID }, domain.CopyGameGiftCatalog, matchGameGiftCatalog),
gameGiftRevisions: newMemoryRepository(func(v domain.GameGiftRevision) string { return v.ID }, domain.CopyGameGiftRevision, matchGameGiftRevision),
gameGiftGrants: newMemoryRepository(func(v domain.GameGiftGrant) string { return v.ID }, domain.CopyGameGiftGrant, matchGameGiftGrant),
gamePlayers: newMemoryRepository(func(v domain.GamePlayer) string { return v.ID }, domain.CopyGamePlayer, matchGamePlayer),
gamePlayerAliases: newMemoryRepository(func(v domain.GamePlayerAlias) string { return v.ID }, domain.CopyGamePlayerAlias, matchGamePlayerAlias),
gamePlayerSessions: newMemoryRepository(func(v domain.GamePlayerSession) string { return v.ID }, domain.CopyGamePlayerSession, matchGamePlayerSession),
gameAccessAttempts: newMemoryRepository(func(v domain.GameAccessAttempt) string { return v.ID }, domain.CopyGameAccessAttempt, matchGameAccessAttempt),
gameSecuritySignals: newMemoryRepository(func(v domain.GameSecuritySignal) string { return v.ID }, domain.CopyGameSecuritySignal, matchGameSecuritySignal),
gamePlayerStatePatches: newMemoryRepository(func(v domain.GamePlayerStatePatch) string { return v.ID }, domain.CopyGamePlayerStatePatch, matchGamePlayerStatePatch),
gameMapTrackPoints: newMemoryRepository(func(v domain.GameMapTrackPoint) string { return v.ID }, domain.CopyGameMapTrackPoint, matchGameMapTrackPoint),
gamePlayerVehicleSegments: newMemoryRepository(func(v domain.GamePlayerVehicleSegment) string { return v.ID }, domain.CopyGamePlayerVehicleSegment, matchGamePlayerVehicleSegment),
gameGiftCatalogs: newMemoryRepository(func(v domain.GameGiftCatalog) string { return v.ID }, domain.CopyGameGiftCatalog, matchGameGiftCatalog),
gameGiftRevisions: newMemoryRepository(func(v domain.GameGiftRevision) string { return v.ID }, domain.CopyGameGiftRevision, matchGameGiftRevision),
gameGiftGrants: newMemoryRepository(func(v domain.GameGiftGrant) string { return v.ID }, domain.CopyGameGiftGrant, matchGameGiftGrant),
}
}
@@ -577,6 +596,12 @@ func (store *MemoryStore) GameSecuritySignals() GameSecuritySignalRepository {
func (store *MemoryStore) GamePlayerStatePatches() GamePlayerStatePatchRepository {
return store.gamePlayerStatePatches
}
func (store *MemoryStore) GameMapTrackPoints() GameMapTrackPointRepository {
return store.gameMapTrackPoints
}
func (store *MemoryStore) GamePlayerVehicleSegments() GamePlayerVehicleSegmentRepository {
return store.gamePlayerVehicleSegments
}
func (store *MemoryStore) GameGiftCatalogs() GameGiftCatalogRepository { return store.gameGiftCatalogs }
func (store *MemoryStore) GameGiftRevisions() GameGiftRevisionRepository {
return store.gameGiftRevisions
@@ -901,6 +926,12 @@ func matchGameSecuritySignal(v domain.GameSecuritySignal, f domain.GameSecurityS
func matchGamePlayerStatePatch(v domain.GamePlayerStatePatch, f domain.GamePlayerStatePatchFilter) bool {
return (f.ServerInstanceID == "" || v.ServerInstanceID == f.ServerInstanceID) && (f.GamePlayerRecordID == "" || v.GamePlayerRecordID == f.GamePlayerRecordID)
}
func matchGameMapTrackPoint(v domain.GameMapTrackPoint, f domain.GameMapTrackPointFilter) bool {
return (f.ServerInstanceID == "" || v.ServerInstanceID == f.ServerInstanceID) && (f.MapID == "" || v.MapID == f.MapID) && (f.MapVersion == "" || v.MapVersion == f.MapVersion) && (f.EntityID == "" || v.EntityID == f.EntityID) && (f.EntityKind == "" || v.EntityKind == f.EntityKind) && (f.OccurredAfter.IsZero() || !v.OccurredAt.Before(f.OccurredAfter)) && (f.OccurredBefore.IsZero() || !v.OccurredAt.After(f.OccurredBefore))
}
func matchGamePlayerVehicleSegment(v domain.GamePlayerVehicleSegment, f domain.GamePlayerVehicleSegmentFilter) bool {
return (f.ServerInstanceID == "" || v.ServerInstanceID == f.ServerInstanceID) && (f.GamePlayerRecordID == "" || v.GamePlayerRecordID == f.GamePlayerRecordID) && (f.VehicleID == "" || v.VehicleID == f.VehicleID) && (f.MapID == "" || v.MapID == f.MapID) && (f.MapVersion == "" || v.MapVersion == f.MapVersion) && (f.OccurredAfter.IsZero() || !v.EndedAt.Before(f.OccurredAfter)) && (f.OccurredBefore.IsZero() || !v.StartedAt.After(f.OccurredBefore))
}
func matchGameGiftCatalog(v domain.GameGiftCatalog, f domain.GameGiftCatalogFilter) bool {
return f.ServerInstanceID == "" || v.ServerInstanceID == f.ServerInstanceID
}
+316
View File
@@ -0,0 +1,316 @@
package service
import (
"math"
"sort"
"strconv"
"strings"
"time"
"browser.local/platform/domain"
"browser.local/platform/repo"
)
const maxMapTrajectoryWindow = 24 * time.Hour
const maxMapTrajectoryEntities = 20
const maxMapTrajectoryPointsPerEntity = 600
func (svc *CoreService) GetGameMapTrajectoriesForSession(sessionID string, query domain.GameMapTrajectoryQuery) (domain.GameMapTrajectoryView, error) {
query = domain.CopyGameMapTrajectoryQuery(query)
if err := svc.authorizeServerLifecycle(sessionID, query.ServerInstanceID); err != nil {
return domain.GameMapTrajectoryView{}, err
}
declaration, err := svc.mapTrajectoryDeclaration(query.ServerInstanceID)
if err != nil {
return domain.GameMapTrajectoryView{}, err
}
if declaration == nil {
return domain.GameMapTrajectoryView{Status: "missing-map", Reason: "此插件未声明兼容的 SCUM 地图、坐标转换或保留策略。"}, nil
}
if query.To.IsZero() {
query.To = svc.now()
}
if query.From.IsZero() {
query.From = query.To.Add(-time.Hour)
}
if query.From.After(query.To) || query.To.Sub(query.From) > maxMapTrajectoryWindow || len(query.PlayerRecordIDs) > maxMapTrajectoryEntities || len(query.VehicleIDs) > maxMapTrajectoryEntities {
return domain.GameMapTrajectoryView{}, validationError("map trajectory window or entity filters are invalid")
}
if err := svc.pruneGameMapTrajectories(query.ServerInstanceID); err != nil {
return domain.GameMapTrajectoryView{}, err
}
view := domain.GameMapTrajectoryView{Status: "ready", Map: *declaration, From: query.From, To: query.To, Players: []domain.GameMapTrajectoryEntity{}, Vehicles: []domain.GameMapTrajectoryEntity{}, RideSegments: []domain.GameMapTrajectorySegment{}}
for _, id := range uniqueBoundedIDs(query.PlayerRecordIDs) {
player, getErr := svc.store.GamePlayers().Get(id)
if getErr == repo.ErrNotFound || player.ServerInstanceID != query.ServerInstanceID {
continue
}
if getErr != nil {
return domain.GameMapTrajectoryView{}, getErr
}
points, listErr := svc.store.GameMapTrackPoints().List(domain.GameMapTrackPointFilter{ServerInstanceID: query.ServerInstanceID, MapID: declaration.MapID, MapVersion: declaration.MapVersion, EntityKind: domain.GameMapTrackEntityPlayer, EntityID: player.GamePlayerID, OccurredAfter: query.From, OccurredBefore: query.To, Limit: maxMapTrajectoryPointsPerEntity})
if listErr != nil {
return domain.GameMapTrajectoryView{}, listErr
}
view.Players = append(view.Players, mapTrajectoryEntity(domain.GameMapTrackEntityPlayer, player.GamePlayerID, player.ID, player.DisplayName, points))
}
for _, id := range uniqueBoundedIDs(query.VehicleIDs) {
points, listErr := svc.store.GameMapTrackPoints().List(domain.GameMapTrackPointFilter{ServerInstanceID: query.ServerInstanceID, MapID: declaration.MapID, MapVersion: declaration.MapVersion, EntityKind: domain.GameMapTrackEntityVehicle, EntityID: id, OccurredAfter: query.From, OccurredBefore: query.To, Limit: maxMapTrajectoryPointsPerEntity})
if listErr != nil {
return domain.GameMapTrajectoryView{}, listErr
}
if len(points) > 0 {
view.Vehicles = append(view.Vehicles, mapTrajectoryEntity(domain.GameMapTrackEntityVehicle, id, "", id, points))
}
}
segments, err := svc.store.GamePlayerVehicleSegments().List(domain.GamePlayerVehicleSegmentFilter{ServerInstanceID: query.ServerInstanceID, MapID: declaration.MapID, MapVersion: declaration.MapVersion, OccurredAfter: query.From, OccurredBefore: query.To, Limit: 200})
if err != nil {
return domain.GameMapTrajectoryView{}, err
}
playerSet, vehicleSet := idSet(query.PlayerRecordIDs), idSet(query.VehicleIDs)
for _, segment := range segments {
if (len(playerSet) == 0 || playerSet[segment.GamePlayerRecordID]) && (len(vehicleSet) == 0 || vehicleSet[segment.VehicleID]) {
view.RideSegments = append(view.RideSegments, domain.GameMapTrajectorySegment{GamePlayerRecordID: segment.GamePlayerRecordID, VehicleID: segment.VehicleID, StartedAt: segment.StartedAt, EndedAt: segment.EndedAt})
}
}
if len(view.Players) == 0 && len(view.Vehicles) == 0 {
view.Status = "empty"
view.Reason = "所选时间窗内没有已采集且兼容当前地图版本的轨迹。"
}
return domain.CopyGameMapTrajectoryView(view), nil
}
func (svc *CoreService) projectGameMapTrajectoryEvents(batch domain.LogBatchIngest) error {
for _, entry := range batch.Entries {
if err := svc.projectGameMapTrajectoryEvent(batch, entry); err != nil {
return err
}
}
return svc.pruneGameMapTrajectories(batch.ServerInstanceID)
}
func (svc *CoreService) projectGameMapTrajectoryEvent(batch domain.LogBatchIngest, entry domain.LogEntry) error {
fields := entry.Fields
if fields == nil {
return nil
}
eventType := strings.TrimSpace(fields["eventType"])
if eventType != "player.position" && eventType != "vehicle.position" && eventType != "player.vehicle.enter" && eventType != "player.vehicle.leave" {
return nil
}
declaration, err := svc.mapTrajectoryDeclaration(batch.ServerInstanceID)
if err != nil || declaration == nil {
return err
}
if strings.TrimSpace(fields["mapId"]) != declaration.MapID || strings.TrimSpace(fields["mapVersion"]) != declaration.MapVersion {
return nil
}
source := strings.TrimSpace(fields["source"])
if source != "companion" && source != "log-projection" {
return nil
}
occurred := mapEventTime(entry, fields, svc.now())
collected := mapCollectedTime(fields, svc.now())
eventID := "map-event-" + entryID(batch.LogStreamID, entry.Seq)
if eventType == "player.position" || eventType == "vehicle.position" {
return svc.projectGameMapPosition(batch.ServerInstanceID, eventID, eventType, fields, occurred, collected, source, *declaration)
}
return svc.projectGameMapVehicleTransition(batch.ServerInstanceID, eventID, eventType, fields, occurred, *declaration)
}
func (svc *CoreService) projectGameMapPosition(serverID, eventID, eventType string, fields map[string]string, occurred, collected time.Time, source string, declaration domain.GameMapTrajectoryDeclaration) error {
entityKind, entityID := domain.GameMapTrackEntityVehicle, strings.TrimSpace(fields["vehicleId"])
playerRecordID := ""
if eventType == "player.position" {
entityKind, entityID = domain.GameMapTrackEntityPlayer, strings.TrimSpace(fields["playerId"])
playerRecordID = gamePlayerRecordID(serverID, entityID)
}
if !mapTrajectoryID(entityID) {
return nil
}
x, okX := mapNumber(fields["worldX"])
y, okY := mapNumber(fields["worldY"])
if !okX || !okY {
return nil
}
mapX, mapY, ok := projectMapPoint(declaration, x, y)
if !ok {
return nil
}
id := "map-point-" + fingerprintID(serverID, eventID)
if _, err := svc.store.GameMapTrackPoints().Get(id); err == nil {
return nil
} else if err != repo.ErrNotFound {
return err
}
existing, err := svc.store.GameMapTrackPoints().List(domain.GameMapTrackPointFilter{ServerInstanceID: serverID, MapID: declaration.MapID, MapVersion: declaration.MapVersion, EntityKind: entityKind, EntityID: entityID})
if err != nil {
return err
}
if compressedMapPoint(existing, occurred, mapX, mapY, declaration) {
return nil
}
return svc.store.GameMapTrackPoints().Create(domain.GameMapTrackPoint{ID: id, EventID: eventID, ServerInstanceID: serverID, MapID: declaration.MapID, MapVersion: declaration.MapVersion, EntityKind: entityKind, EntityID: entityID, GamePlayerRecordID: playerRecordID, MapX: mapX, MapY: mapY, Source: source, OccurredAt: occurred, CollectedAt: collected, ExpiresAt: occurred.Add(time.Duration(declaration.RetentionSeconds) * time.Second)})
}
func (svc *CoreService) projectGameMapVehicleTransition(serverID, eventID, eventType string, fields map[string]string, occurred time.Time, declaration domain.GameMapTrajectoryDeclaration) error {
playerID, vehicleID := strings.TrimSpace(fields["playerId"]), strings.TrimSpace(fields["vehicleId"])
if !mapTrajectoryID(playerID) || !mapTrajectoryID(vehicleID) {
return nil
}
playerRecordID := gamePlayerRecordID(serverID, playerID)
id := "map-ride-" + fingerprintID(serverID, eventID)
if _, err := svc.store.GamePlayerVehicleSegments().Get(id); err == nil {
return nil
} else if err != repo.ErrNotFound {
return err
}
segments, err := svc.store.GamePlayerVehicleSegments().List(domain.GamePlayerVehicleSegmentFilter{ServerInstanceID: serverID, GamePlayerRecordID: playerRecordID, MapID: declaration.MapID, MapVersion: declaration.MapVersion})
if err != nil {
return err
}
for _, segment := range segments {
if segment.EndedAt.IsZero() && !segment.StartedAt.After(occurred) && (eventType == "player.vehicle.enter" || segment.VehicleID == vehicleID) {
segment.EndedAt = occurred
if err := svc.store.GamePlayerVehicleSegments().Update(segment); err != nil {
return err
}
}
}
if eventType == "player.vehicle.leave" {
return nil
}
return svc.store.GamePlayerVehicleSegments().Create(domain.GamePlayerVehicleSegment{ID: id, EventID: eventID, ServerInstanceID: serverID, GamePlayerRecordID: playerRecordID, GamePlayerID: playerID, VehicleID: vehicleID, MapID: declaration.MapID, MapVersion: declaration.MapVersion, StartedAt: occurred, ExpiresAt: occurred.Add(time.Duration(declaration.RetentionSeconds) * time.Second)})
}
func (svc *CoreService) pruneGameMapTrajectories(serverID string) error {
now := svc.now()
points, err := svc.store.GameMapTrackPoints().List(domain.GameMapTrackPointFilter{ServerInstanceID: serverID})
if err != nil {
return err
}
for _, point := range points {
if !point.ExpiresAt.After(now) {
if err := svc.store.GameMapTrackPoints().Delete(point.ID); err != nil {
return err
}
}
}
segments, err := svc.store.GamePlayerVehicleSegments().List(domain.GamePlayerVehicleSegmentFilter{ServerInstanceID: serverID})
if err != nil {
return err
}
for _, segment := range segments {
if !segment.ExpiresAt.After(now) {
if err := svc.store.GamePlayerVehicleSegments().Delete(segment.ID); err != nil {
return err
}
}
}
return nil
}
func (svc *CoreService) mapTrajectoryDeclaration(serverID string) (*domain.GameMapTrajectoryDeclaration, error) {
instance, err := svc.store.ServerInstances().Get(serverID)
if err != nil {
return nil, err
}
plugin, err := svc.store.GamePlugins().Get(instance.PluginID)
if err != nil {
return nil, err
}
if plugin.MapTrajectories == nil {
return nil, nil
}
declaration := domain.CopyGameMapTrajectoryDeclaration(*plugin.MapTrajectories)
if !validMapTrajectoryDeclaration(declaration) {
return nil, validationError("plugin map trajectory declaration is invalid")
}
return &declaration, nil
}
func validMapTrajectoryDeclaration(v domain.GameMapTrajectoryDeclaration) bool {
return v.MapID != "" && v.MapVersion != "" && v.WorldMaxX > v.WorldMinX && v.WorldMaxY > v.WorldMinY && v.ImageWidth > 0 && v.ImageHeight > 0 && v.Precision > 0 && v.SampleDistance >= 0 && v.SampleIntervalSeconds >= 0 && v.RetentionSeconds > 0 && v.RetentionSeconds <= 31*24*60*60
}
func projectMapPoint(v domain.GameMapTrajectoryDeclaration, x, y float64) (float64, float64, bool) {
if !finite(x) || !finite(y) || x < v.WorldMinX || x > v.WorldMaxX || y < v.WorldMinY || y > v.WorldMaxY {
return 0, 0, false
}
return roundMap((x-v.WorldMinX)/(v.WorldMaxX-v.WorldMinX)*1000, v.Precision), roundMap((y-v.WorldMinY)/(v.WorldMaxY-v.WorldMinY)*1000, v.Precision), true
}
func compressedMapPoint(points []domain.GameMapTrackPoint, occurred time.Time, x, y float64, declaration domain.GameMapTrajectoryDeclaration) bool {
var prior *domain.GameMapTrackPoint
for i := range points {
if !points[i].OccurredAt.After(occurred) && (prior == nil || points[i].OccurredAt.After(prior.OccurredAt)) {
prior = &points[i]
}
}
if prior == nil {
return false
}
seconds := occurred.Sub(prior.OccurredAt).Seconds()
distance := math.Hypot(x-prior.MapX, y-prior.MapY)
return seconds < float64(declaration.SampleIntervalSeconds) && distance < declaration.SampleDistance
}
func mapTrajectoryEntity(kind domain.GameMapTrackEntityKind, entityID, playerID, label string, points []domain.GameMapTrackPoint) domain.GameMapTrajectoryEntity {
sort.Slice(points, func(i, j int) bool { return points[i].OccurredAt.Before(points[j].OccurredAt) })
sources := map[string]struct{}{}
var collected time.Time
for _, point := range points {
sources[point.Source] = struct{}{}
if point.CollectedAt.After(collected) {
collected = point.CollectedAt
}
}
values := make([]string, 0, len(sources))
for source := range sources {
values = append(values, source)
}
sort.Strings(values)
return domain.GameMapTrajectoryEntity{Kind: kind, EntityID: entityID, GamePlayerRecordID: playerID, Label: label, Points: points, CollectedAt: collected, Sources: values}
}
func mapEventTime(entry domain.LogEntry, fields map[string]string, fallback time.Time) time.Time {
if value, err := time.Parse(time.RFC3339, strings.TrimSpace(fields["occurredAt"])); err == nil {
return value
}
if !entry.Timestamp.IsZero() {
return entry.Timestamp
}
return fallback
}
func mapCollectedTime(fields map[string]string, fallback time.Time) time.Time {
if value, err := time.Parse(time.RFC3339, strings.TrimSpace(fields["collectedAt"])); err == nil {
return value
}
return fallback
}
func mapNumber(value string) (float64, bool) {
number, err := strconv.ParseFloat(strings.TrimSpace(value), 64)
return number, err == nil && finite(number)
}
func finite(value float64) bool { return !math.IsNaN(value) && !math.IsInf(value, 0) }
func roundMap(value, precision float64) float64 { return math.Round(value/precision) * precision }
func mapTrajectoryID(value string) bool {
value = strings.TrimSpace(value)
if value == "" || len(value) > 96 {
return false
}
for _, char := range value {
if !(char >= 'a' && char <= 'z' || char >= 'A' && char <= 'Z' || char >= '0' && char <= '9' || char == '-' || char == '_' || char == '.' || char == ':') {
return false
}
}
return true
}
func uniqueBoundedIDs(values []string) []string {
seen := map[string]bool{}
result := make([]string, 0, len(values))
for _, value := range values {
if mapTrajectoryID(value) && !seen[value] {
seen[value] = true
result = append(result, value)
}
}
return result
}
func idSet(values []string) map[string]bool {
result := map[string]bool{}
for _, value := range uniqueBoundedIDs(values) {
result[value] = true
}
return result
}
@@ -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)
}
}
+6
View File
@@ -35,6 +35,9 @@ func (svc *CoreService) IngestLogBatch(batch domain.LogBatchIngest) (domain.LogB
if err := svc.projectGamePlayerEvents(projectionBatch); err != nil {
return domain.LogBatchIngestResult{}, err
}
if err := svc.projectGameMapTrajectoryEvents(projectionBatch); err != nil {
return domain.LogBatchIngestResult{}, err
}
return domain.LogBatchIngestResult{
Accepted: true,
LogStreamID: batch.LogStreamID,
@@ -70,6 +73,9 @@ func (svc *CoreService) IngestLogBatch(batch domain.LogBatchIngest) (domain.LogB
if err := svc.projectGamePlayerEvents(projectionBatch); err != nil {
return domain.LogBatchIngestResult{}, err
}
if err := svc.projectGameMapTrajectoryEvents(projectionBatch); err != nil {
return domain.LogBatchIngestResult{}, err
}
return domain.LogBatchIngestResult{
Accepted: true,
LogStreamID: batch.LogStreamID,
+3
View File
@@ -205,6 +205,7 @@ type Core interface {
QueryLogStream(domain.LogStreamCursorQuery) (domain.LogStreamCursorResult, error)
ListGamePlayersForSession(string, domain.GamePlayerFilter) ([]domain.GamePlayer, error)
GetGamePlayerProfileForSession(string, string) (domain.GamePlayerProfile, error)
GetGameMapTrajectoriesForSession(string, domain.GameMapTrajectoryQuery) (domain.GameMapTrajectoryView, error)
GetGamePlayerStateForSession(string, string) (domain.GamePlayerStateSnapshot, error)
RequestGamePlayerStatePatchForSession(string, string, domain.GamePlayerStatePatchRequest) (domain.GamePlayerStatePatch, error)
ApproveGamePlayerStatePatchForSession(string, string) (domain.GamePlayerStatePatch, error)
@@ -726,6 +727,7 @@ func gamePluginFromManifestRegistration(registration domain.GamePluginManifestRe
RemoteAccess: manifest.RemoteAccess,
RuntimeProfiles: manifest.RuntimeProfiles,
GameClientBridge: manifest.GameClientBridge,
MapTrajectories: manifest.MapTrajectories,
Status: domain.GamePluginStatusInstalled,
}
}
@@ -1492,6 +1494,7 @@ func marketplacePluginFromGamePlugin(plugin domain.GamePlugin) domain.PluginMark
RemoteAccess: plugin.RemoteAccess,
RuntimeProfiles: plugin.RuntimeProfiles,
GameClientBridge: plugin.GameClientBridge,
MapTrajectories: plugin.MapTrajectories,
ValidationViolations: plugin.ValidationViolations,
Status: plugin.Status,
Source: "platform-registry",
+12
View File
@@ -157,6 +157,7 @@ func ValidateGamePlugin(plugin domain.GamePlugin) error {
violations = append(violations, validateRuntimeProfileCapabilityDeclarations(plugin.RuntimeProfiles, plugin.RequiredRunCapabilities)...)
violations = append(violations, validateRuntimeLogEventPermissionDeclarations("runtimeProfiles.logEvents", plugin.RuntimeProfiles, plugin.DeclaredPermissions)...)
violations = append(violations, validateGameClientBridgeManifest("gameClientBridge", plugin.GameClientBridge, plugin.DeclaredPermissions, plugin.Pages, plugin.RuntimeProfiles)...)
violations = append(violations, validateMapTrajectoryDeclaration("mapTrajectories", plugin.MapTrajectories)...)
violations = append(violations, validatePluginCreateFields("createFields", plugin.CreateFields)...)
violations = append(violations, validateSafePluginStrings("gamePlugin", pluginSafeStrings(plugin))...)
return finish(violations)
@@ -227,10 +228,21 @@ func ValidateGamePluginManifestRegistration(registration domain.GamePluginManife
violations = append(violations, validateRuntimeProfileCapabilityDeclarations(manifest.RuntimeProfiles, manifest.Capabilities)...)
violations = append(violations, validateRuntimeLogEventPermissionDeclarations("manifest.runtimeProfiles.logEvents", manifest.RuntimeProfiles, manifest.Permissions)...)
violations = append(violations, validateGameClientBridgeManifest("manifest.gameClientBridge", manifest.GameClientBridge, manifest.Permissions, manifest.Pages, manifest.RuntimeProfiles)...)
violations = append(violations, validateMapTrajectoryDeclaration("manifest.mapTrajectories", manifest.MapTrajectories)...)
violations = append(violations, validateSafePluginStrings("manifest", manifestSafeStrings(registration))...)
return finish(violations)
}
func validateMapTrajectoryDeclaration(prefix string, value *domain.GameMapTrajectoryDeclaration) []string {
if value == nil {
return nil
}
if value.MapID == "" || value.MapVersion == "" || value.WorldMaxX <= value.WorldMinX || value.WorldMaxY <= value.WorldMinY || value.ImageWidth <= 0 || value.ImageHeight <= 0 || value.Precision <= 0 || value.SampleDistance < 0 || value.SampleIntervalSeconds < 0 || value.RetentionSeconds <= 0 || value.RetentionSeconds > 31*24*60*60 {
return []string{prefix + " is invalid"}
}
return nil
}
func validatePluginCreateFields(prefix string, fields []domain.PluginCreateField) []string {
if len(fields) > 32 {
return []string{prefix + " must contain at most 32 fields"}