Remove SCUM sqlite user projections
This commit is contained in:
@@ -79,6 +79,11 @@ func (h *coreHandlers) register(mux *http.ServeMux) {
|
||||
mux.HandleFunc("/api/v1/server-instances/{id}/game-client-bridge/commands/{commandId}/cancel", h.serverGameClientBridgeCommandCancel)
|
||||
mux.HandleFunc("/api/v1/server-instances/{id}/game-client-bridge/commands/{commandId}", h.serverGameClientBridgeCommandDetail)
|
||||
mux.HandleFunc("/api/v1/server-instances/{id}/game-client-bridge/snapshots", h.serverGameClientBridgeSnapshots)
|
||||
mux.HandleFunc("/api/v1/server-instances/{id}/scum/users", h.serverSCUMUsers)
|
||||
mux.HandleFunc("/api/v1/server-instances/{id}/scum/user-trajectories", h.serverSCUMUserTrajectories)
|
||||
mux.HandleFunc("/api/v1/server-instances/{id}/scum/vehicles", h.serverSCUMVehicles)
|
||||
mux.HandleFunc("/api/v1/server-instances/{id}/scum/vehicle-trajectories", h.serverSCUMVehicleTrajectories)
|
||||
mux.HandleFunc("/api/v1/server-instances/{id}/scum/vehicle-locks", h.serverSCUMVehicleLocks)
|
||||
mux.HandleFunc("/api/v1/server-instances/{id}/plugin-data/{collection}", h.serverPluginDataCollection)
|
||||
mux.HandleFunc("/api/v1/server-instances/{id}/plugin-data/{collection}/transaction", h.serverPluginDataTransaction)
|
||||
mux.HandleFunc("/api/v1/server-instances/{id}/dependencies/check", h.serverDependenciesCheck)
|
||||
@@ -103,6 +108,7 @@ func (h *coreHandlers) register(mux *http.ServeMux) {
|
||||
mux.HandleFunc("/api/v1/run/control/heartbeat", h.requireRunSignature(h.runControlHeartbeat))
|
||||
mux.HandleFunc("/api/v1/run/control/events", h.requireRunSignature(h.runControlEvents))
|
||||
mux.HandleFunc("/api/v1/run/lifecycle/report", h.requireRunSignature(h.runLifecycleReport))
|
||||
mux.HandleFunc("/api/v1/run/scum/facts", h.requireRunSignature(h.runSCUMFacts))
|
||||
mux.HandleFunc("/api/v1/run/jobs/claim", h.requireRunSignature(h.runJobClaim))
|
||||
mux.HandleFunc("/api/v1/run/jobs/ack", h.requireRunSignature(h.runJobAck))
|
||||
mux.HandleFunc("/api/v1/run/jobs/progress", h.requireRunSignature(h.runJobProgress))
|
||||
|
||||
@@ -154,7 +154,7 @@ Server-scoped terminal log streaming (`GET /api/v1/server-instances/{id}/logs/ev
|
||||
|
||||
Runtime distribution APIs require the current bearer session, server visibility, plugin-declared permissions, complete runtime bindings only for actions that truly depend on external logical bindings, and platform-builder readiness. Run-side lifecycle commands separately require run endpoint capability support and use plugin-declared lifecycle actions without making manual runtime-profile binding a user prerequisite. Responses and summaries expose artifact IDs, job IDs, checksums, key generations, fingerprints, status, and redacted `secret://runtime-keys/.../current` refs only. They do not expose raw run keys, FTP passwords, database DSNs, RCON passwords, host paths, direct sockets, run endpoint private addresses, build workspace paths, or large inline logs.
|
||||
|
||||
SCUM product APIs expose only safe local projections, typed operation/workflow requests, approval status, confirmation status, blocker reasons, and bounded summaries. They never expose SCUM.db SQL text, DB paths, DSNs, RCON command text, raw request payloads, run sockets, host paths, or credentials.
|
||||
SCUM product APIs expose platform-owned `scum_user`, trajectory, vehicle, and lock table records plus typed operation/workflow requests, approval status, confirmation status, blocker reasons, and bounded summaries. They never expose direct game database SQL text, DB paths, DSNs, RCON command text, raw request payloads, run sockets, host paths, or credentials.
|
||||
|
||||
`POST /api/v1/server-instances/workflows/create` requires only the plugin type and server name. A runtime binding may still be maintained internally for advanced logical transports, but browser lifecycle controls must not force operators to choose a runtime profile before start/stop or run-package generation when the plugin deployment/lifecycle declaration is sufficient. Platform builds distributions itself and never needs a registered Run endpoint with `distribution.build` to do so.
|
||||
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
"browser.local/platform/dto"
|
||||
)
|
||||
|
||||
func (h *coreHandlers) serverSCUMUsers(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
writeMethodNotAllowed(w, http.MethodGet)
|
||||
return
|
||||
}
|
||||
filter, ok := scumUserFilterFromRequest(w, r, r.PathValue("id"))
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
items, err := h.core.ListSCUMUsersForSession(bearerToken(r), filter)
|
||||
if err != nil {
|
||||
writeServiceError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, dto.SCUMUserListFromDomain(items))
|
||||
}
|
||||
|
||||
func (h *coreHandlers) serverSCUMUserTrajectories(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
writeMethodNotAllowed(w, http.MethodGet)
|
||||
return
|
||||
}
|
||||
filter, ok := scumUserTrajectoryFilterFromRequest(w, r, r.PathValue("id"))
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
items, err := h.core.ListSCUMUserTrajectoriesForSession(bearerToken(r), filter)
|
||||
if err != nil {
|
||||
writeServiceError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, dto.SCUMUserTrajectoryListFromDomain(items))
|
||||
}
|
||||
|
||||
func (h *coreHandlers) serverSCUMVehicles(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
writeMethodNotAllowed(w, http.MethodGet)
|
||||
return
|
||||
}
|
||||
filter, ok := scumVehicleFilterFromRequest(w, r, r.PathValue("id"))
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
items, err := h.core.ListSCUMVehiclesForSession(bearerToken(r), filter)
|
||||
if err != nil {
|
||||
writeServiceError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, dto.SCUMVehicleListFromDomain(items))
|
||||
}
|
||||
|
||||
func (h *coreHandlers) serverSCUMVehicleTrajectories(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
writeMethodNotAllowed(w, http.MethodGet)
|
||||
return
|
||||
}
|
||||
filter, ok := scumVehicleTrajectoryFilterFromRequest(w, r, r.PathValue("id"))
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
items, err := h.core.ListSCUMVehicleTrajectoriesForSession(bearerToken(r), filter)
|
||||
if err != nil {
|
||||
writeServiceError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, dto.SCUMVehicleTrajectoryListFromDomain(items))
|
||||
}
|
||||
|
||||
func (h *coreHandlers) serverSCUMVehicleLocks(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
writeMethodNotAllowed(w, http.MethodGet)
|
||||
return
|
||||
}
|
||||
filter, ok := scumVehicleLockFilterFromRequest(w, r, r.PathValue("id"))
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
items, err := h.core.ListSCUMVehicleLocksForSession(bearerToken(r), filter)
|
||||
if err != nil {
|
||||
writeServiceError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, dto.SCUMVehicleLockListFromDomain(items))
|
||||
}
|
||||
|
||||
func (h *coreHandlers) runSCUMFacts(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
writeMethodNotAllowed(w, http.MethodPost)
|
||||
return
|
||||
}
|
||||
request, err := decodeJSON[dto.SCUMFactIngestRequest](r)
|
||||
if err != nil {
|
||||
writeDecodeError(w, err)
|
||||
return
|
||||
}
|
||||
result, err := h.core.IngestSCUMFacts(request.ToDomain())
|
||||
if err != nil {
|
||||
writeServiceError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusAccepted, dto.SCUMFactIngestFromDomain(result))
|
||||
}
|
||||
|
||||
func scumUserFilterFromRequest(w http.ResponseWriter, r *http.Request, serverID string) (domain.SCUMUserFilter, bool) {
|
||||
limit, ok := optionalLimitFromRequest(w, r)
|
||||
if !ok {
|
||||
return domain.SCUMUserFilter{}, false
|
||||
}
|
||||
online, ok := optionalBoolFromRequest(w, r, "online")
|
||||
if !ok {
|
||||
return domain.SCUMUserFilter{}, false
|
||||
}
|
||||
changedAfter, ok := optionalTimeFromRequest(w, r, "changedAfter")
|
||||
if !ok {
|
||||
return domain.SCUMUserFilter{}, false
|
||||
}
|
||||
return domain.SCUMUserFilter{ServerInstanceID: serverID, SteamID: r.URL.Query().Get("steamId"), Online: online, ChangedAfter: changedAfter, Limit: limit}, true
|
||||
}
|
||||
|
||||
func scumUserTrajectoryFilterFromRequest(w http.ResponseWriter, r *http.Request, serverID string) (domain.SCUMUserTrajectoryFilter, bool) {
|
||||
limit, ok := optionalLimitFromRequest(w, r)
|
||||
if !ok {
|
||||
return domain.SCUMUserTrajectoryFilter{}, false
|
||||
}
|
||||
after, ok := optionalTimeFromRequest(w, r, "after")
|
||||
if !ok {
|
||||
return domain.SCUMUserTrajectoryFilter{}, false
|
||||
}
|
||||
return domain.SCUMUserTrajectoryFilter{ServerInstanceID: serverID, SCUMUserID: r.URL.Query().Get("scumUserId"), SteamID: r.URL.Query().Get("steamId"), After: after, Limit: limit}, true
|
||||
}
|
||||
|
||||
func scumVehicleFilterFromRequest(w http.ResponseWriter, r *http.Request, serverID string) (domain.SCUMVehicleFilter, bool) {
|
||||
limit, ok := optionalLimitFromRequest(w, r)
|
||||
if !ok {
|
||||
return domain.SCUMVehicleFilter{}, false
|
||||
}
|
||||
exists, ok := optionalBoolFromRequest(w, r, "exists")
|
||||
if !ok {
|
||||
return domain.SCUMVehicleFilter{}, false
|
||||
}
|
||||
changedAfter, ok := optionalTimeFromRequest(w, r, "changedAfter")
|
||||
if !ok {
|
||||
return domain.SCUMVehicleFilter{}, false
|
||||
}
|
||||
return domain.SCUMVehicleFilter{ServerInstanceID: serverID, GameVehicleID: r.URL.Query().Get("gameVehicleId"), Exists: exists, ChangedAfter: changedAfter, Limit: limit}, true
|
||||
}
|
||||
|
||||
func scumVehicleTrajectoryFilterFromRequest(w http.ResponseWriter, r *http.Request, serverID string) (domain.SCUMVehicleTrajectoryFilter, bool) {
|
||||
limit, ok := optionalLimitFromRequest(w, r)
|
||||
if !ok {
|
||||
return domain.SCUMVehicleTrajectoryFilter{}, false
|
||||
}
|
||||
after, ok := optionalTimeFromRequest(w, r, "after")
|
||||
if !ok {
|
||||
return domain.SCUMVehicleTrajectoryFilter{}, false
|
||||
}
|
||||
return domain.SCUMVehicleTrajectoryFilter{ServerInstanceID: serverID, SCUMVehicleID: r.URL.Query().Get("scumVehicleId"), GameVehicleID: r.URL.Query().Get("gameVehicleId"), After: after, Limit: limit}, true
|
||||
}
|
||||
|
||||
func scumVehicleLockFilterFromRequest(w http.ResponseWriter, r *http.Request, serverID string) (domain.SCUMVehicleLockFilter, bool) {
|
||||
limit, ok := optionalLimitFromRequest(w, r)
|
||||
if !ok {
|
||||
return domain.SCUMVehicleLockFilter{}, false
|
||||
}
|
||||
after, ok := optionalTimeFromRequest(w, r, "after")
|
||||
if !ok {
|
||||
return domain.SCUMVehicleLockFilter{}, false
|
||||
}
|
||||
query := r.URL.Query()
|
||||
return domain.SCUMVehicleLockFilter{ServerInstanceID: serverID, SCUMVehicleID: query.Get("scumVehicleId"), GameVehicleID: query.Get("gameVehicleId"), SCUMUserID: query.Get("scumUserId"), SteamID: query.Get("steamId"), After: after, Limit: limit}, true
|
||||
}
|
||||
|
||||
func optionalLimitFromRequest(w http.ResponseWriter, r *http.Request) (int, bool) {
|
||||
limit, err := optionalPositiveInt(r.URL.Query().Get("limit"))
|
||||
if err != nil || limit < 0 {
|
||||
writeAPIError(w, http.StatusBadRequest, errorCodeBadRequest, "invalid limit", nil)
|
||||
return 0, false
|
||||
}
|
||||
return limit, true
|
||||
}
|
||||
|
||||
func optionalBoolFromRequest(w http.ResponseWriter, r *http.Request, key string) (*bool, bool) {
|
||||
value := r.URL.Query().Get(key)
|
||||
if value == "" {
|
||||
return nil, true
|
||||
}
|
||||
parsed, err := strconv.ParseBool(value)
|
||||
if err != nil {
|
||||
writeAPIError(w, http.StatusBadRequest, errorCodeBadRequest, "invalid "+key, nil)
|
||||
return nil, false
|
||||
}
|
||||
return &parsed, true
|
||||
}
|
||||
|
||||
func optionalTimeFromRequest(w http.ResponseWriter, r *http.Request, key string) (time.Time, bool) {
|
||||
value := r.URL.Query().Get(key)
|
||||
if value == "" {
|
||||
return time.Time{}, true
|
||||
}
|
||||
parsed, err := time.Parse(time.RFC3339, value)
|
||||
if err != nil {
|
||||
writeAPIError(w, http.StatusBadRequest, errorCodeBadRequest, "invalid "+key, nil)
|
||||
return time.Time{}, false
|
||||
}
|
||||
return parsed, true
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
package domain
|
||||
|
||||
import "time"
|
||||
|
||||
const (
|
||||
SCUMUserOfflineAfter = 15 * time.Minute
|
||||
SCUMWelcomeMessageDelay = time.Minute
|
||||
SCUMDefaultListLimit = 500
|
||||
SCUMDefaultTrajectoryLimit = 500
|
||||
)
|
||||
|
||||
type SCUMPosition struct {
|
||||
X float64
|
||||
Y float64
|
||||
Z float64
|
||||
}
|
||||
|
||||
type SCUMUser struct {
|
||||
ID string
|
||||
ServerInstanceID string
|
||||
SteamID string
|
||||
DisplayName string
|
||||
SquadID int64
|
||||
RegisteredAt time.Time
|
||||
LastLoginAt time.Time
|
||||
LastActivityAt time.Time
|
||||
LastLoginIP string
|
||||
Online bool
|
||||
X *float64
|
||||
Y *float64
|
||||
Z *float64
|
||||
BankBalance *int64
|
||||
GoldBars *int64
|
||||
RiddenVehicleID string
|
||||
GameVehicleID string
|
||||
LastLoginAge time.Duration
|
||||
WelcomeQueuedAt time.Time
|
||||
WelcomeLastError string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type SCUMUserFilter struct {
|
||||
ServerInstanceID string
|
||||
SteamID string
|
||||
Online *bool
|
||||
ChangedAfter time.Time
|
||||
Limit int
|
||||
}
|
||||
|
||||
type SCUMUserTrajectory struct {
|
||||
ID string
|
||||
ServerInstanceID string
|
||||
SCUMUserID string
|
||||
SteamID string
|
||||
DisplayName string
|
||||
X float64
|
||||
Y float64
|
||||
Z float64
|
||||
RiddenVehicleID string
|
||||
GameVehicleID string
|
||||
ObservedAt time.Time
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
type SCUMUserTrajectoryFilter struct {
|
||||
ServerInstanceID string
|
||||
SCUMUserID string
|
||||
SteamID string
|
||||
After time.Time
|
||||
Limit int
|
||||
}
|
||||
|
||||
type SCUMVehicle struct {
|
||||
ID string
|
||||
ServerInstanceID string
|
||||
GameVehicleID string
|
||||
VehicleClass string
|
||||
DisplayName string
|
||||
Exists bool
|
||||
Locked bool
|
||||
X *float64
|
||||
Y *float64
|
||||
Z *float64
|
||||
LastObservedAt time.Time
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type SCUMVehicleFilter struct {
|
||||
ServerInstanceID string
|
||||
GameVehicleID string
|
||||
Exists *bool
|
||||
ChangedAfter time.Time
|
||||
Limit int
|
||||
}
|
||||
|
||||
type SCUMVehicleTrajectory struct {
|
||||
ID string
|
||||
ServerInstanceID string
|
||||
SCUMVehicleID string
|
||||
GameVehicleID string
|
||||
VehicleClass string
|
||||
X float64
|
||||
Y float64
|
||||
Z float64
|
||||
ObservedAt time.Time
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
type SCUMVehicleTrajectoryFilter struct {
|
||||
ServerInstanceID string
|
||||
SCUMVehicleID string
|
||||
GameVehicleID string
|
||||
After time.Time
|
||||
Limit int
|
||||
}
|
||||
|
||||
type SCUMVehicleLock struct {
|
||||
ID string
|
||||
ServerInstanceID string
|
||||
SCUMVehicleID string
|
||||
GameVehicleID string
|
||||
SCUMUserID string
|
||||
SteamID string
|
||||
LockedAt time.Time
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
type SCUMVehicleLockFilter struct {
|
||||
ServerInstanceID string
|
||||
SCUMVehicleID string
|
||||
GameVehicleID string
|
||||
SCUMUserID string
|
||||
SteamID string
|
||||
After time.Time
|
||||
Limit int
|
||||
}
|
||||
|
||||
type SCUMFactIngest struct {
|
||||
RunEndpointID string
|
||||
SessionToken string
|
||||
ServerInstanceID string
|
||||
Users []SCUMUserFact
|
||||
Vehicles []SCUMVehicleFact
|
||||
}
|
||||
|
||||
type SCUMUserFact struct {
|
||||
SteamID string
|
||||
DisplayName string
|
||||
SquadID *int64
|
||||
LoginIP string
|
||||
Online bool
|
||||
Login bool
|
||||
ObservedAt time.Time
|
||||
LoginObservedAt time.Time
|
||||
Position *SCUMPosition
|
||||
BankBalance *int64
|
||||
GoldBars *int64
|
||||
RiddenGameVehicleID string
|
||||
}
|
||||
|
||||
type SCUMVehicleFact struct {
|
||||
GameVehicleID string
|
||||
VehicleClass string
|
||||
DisplayName string
|
||||
Exists *bool
|
||||
Locked *bool
|
||||
LockedBySteamID string
|
||||
LockedAt time.Time
|
||||
ObservedAt time.Time
|
||||
Position *SCUMPosition
|
||||
}
|
||||
|
||||
type SCUMFactIngestResult struct {
|
||||
Accepted bool
|
||||
AcceptedUserCount int
|
||||
AcceptedVehicleCount int
|
||||
WelcomeQueuedCount int
|
||||
ServerTime time.Time
|
||||
}
|
||||
|
||||
func CopySCUMUser(value SCUMUser) SCUMUser {
|
||||
value.X = copyFloatPtr(value.X)
|
||||
value.Y = copyFloatPtr(value.Y)
|
||||
value.Z = copyFloatPtr(value.Z)
|
||||
value.BankBalance = copyInt64Ptr(value.BankBalance)
|
||||
value.GoldBars = copyInt64Ptr(value.GoldBars)
|
||||
return value
|
||||
}
|
||||
|
||||
func CopySCUMUsers(values []SCUMUser) []SCUMUser {
|
||||
if values == nil {
|
||||
return nil
|
||||
}
|
||||
out := make([]SCUMUser, len(values))
|
||||
for i, value := range values {
|
||||
out[i] = CopySCUMUser(value)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func CopySCUMUserTrajectory(value SCUMUserTrajectory) SCUMUserTrajectory { return value }
|
||||
func CopySCUMUserTrajectories(values []SCUMUserTrajectory) []SCUMUserTrajectory {
|
||||
if values == nil {
|
||||
return nil
|
||||
}
|
||||
out := make([]SCUMUserTrajectory, len(values))
|
||||
copy(out, values)
|
||||
return out
|
||||
}
|
||||
|
||||
func CopySCUMVehicle(value SCUMVehicle) SCUMVehicle {
|
||||
value.X = copyFloatPtr(value.X)
|
||||
value.Y = copyFloatPtr(value.Y)
|
||||
value.Z = copyFloatPtr(value.Z)
|
||||
return value
|
||||
}
|
||||
|
||||
func CopySCUMVehicles(values []SCUMVehicle) []SCUMVehicle {
|
||||
if values == nil {
|
||||
return nil
|
||||
}
|
||||
out := make([]SCUMVehicle, len(values))
|
||||
for i, value := range values {
|
||||
out[i] = CopySCUMVehicle(value)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func CopySCUMVehicleTrajectory(value SCUMVehicleTrajectory) SCUMVehicleTrajectory { return value }
|
||||
func CopySCUMVehicleTrajectories(values []SCUMVehicleTrajectory) []SCUMVehicleTrajectory {
|
||||
if values == nil {
|
||||
return nil
|
||||
}
|
||||
out := make([]SCUMVehicleTrajectory, len(values))
|
||||
copy(out, values)
|
||||
return out
|
||||
}
|
||||
|
||||
func CopySCUMVehicleLock(value SCUMVehicleLock) SCUMVehicleLock { return value }
|
||||
func CopySCUMVehicleLocks(values []SCUMVehicleLock) []SCUMVehicleLock {
|
||||
if values == nil {
|
||||
return nil
|
||||
}
|
||||
out := make([]SCUMVehicleLock, len(values))
|
||||
copy(out, values)
|
||||
return out
|
||||
}
|
||||
|
||||
func CopySCUMFactIngest(value SCUMFactIngest) SCUMFactIngest {
|
||||
value.Users = CopySCUMUserFacts(value.Users)
|
||||
value.Vehicles = CopySCUMVehicleFacts(value.Vehicles)
|
||||
return value
|
||||
}
|
||||
|
||||
func CopySCUMUserFacts(values []SCUMUserFact) []SCUMUserFact {
|
||||
if values == nil {
|
||||
return nil
|
||||
}
|
||||
out := make([]SCUMUserFact, len(values))
|
||||
for i, value := range values {
|
||||
out[i] = value
|
||||
out[i].SquadID = copyInt64Ptr(value.SquadID)
|
||||
out[i].BankBalance = copyInt64Ptr(value.BankBalance)
|
||||
out[i].GoldBars = copyInt64Ptr(value.GoldBars)
|
||||
if value.Position != nil {
|
||||
position := *value.Position
|
||||
out[i].Position = &position
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func CopySCUMVehicleFacts(values []SCUMVehicleFact) []SCUMVehicleFact {
|
||||
if values == nil {
|
||||
return nil
|
||||
}
|
||||
out := make([]SCUMVehicleFact, len(values))
|
||||
for i, value := range values {
|
||||
out[i] = value
|
||||
out[i].Exists = copyBoolPtr(value.Exists)
|
||||
out[i].Locked = copyBoolPtr(value.Locked)
|
||||
if value.Position != nil {
|
||||
position := *value.Position
|
||||
out[i].Position = &position
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func copyInt64Ptr(value *int64) *int64 {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
copy := *value
|
||||
return ©
|
||||
}
|
||||
|
||||
func copyBoolPtr(value *bool) *bool {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
copy := *value
|
||||
return ©
|
||||
}
|
||||
@@ -0,0 +1,289 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
)
|
||||
|
||||
type SCUMPositionBody struct {
|
||||
X float64 `json:"x"`
|
||||
Y float64 `json:"y"`
|
||||
Z float64 `json:"z"`
|
||||
}
|
||||
|
||||
type SCUMUserResponse struct {
|
||||
ID string `json:"id"`
|
||||
ServerInstanceID string `json:"serverInstanceId"`
|
||||
SteamID string `json:"steamId"`
|
||||
GamePlayerID string `json:"gamePlayerId"`
|
||||
DisplayName string `json:"displayName,omitempty"`
|
||||
SquadID int64 `json:"squadId,omitempty"`
|
||||
RegisteredAt time.Time `json:"registeredAt"`
|
||||
LastLoginAt time.Time `json:"lastLoginAt"`
|
||||
LastSeenAt time.Time `json:"lastSeenAt"`
|
||||
LastActivityAt time.Time `json:"lastActivityAt"`
|
||||
LastLoginAgeSeconds int64 `json:"lastLoginAgeSeconds,omitempty"`
|
||||
LastLoginIP string `json:"lastLoginIp,omitempty"`
|
||||
LoginIP string `json:"loginIp,omitempty"`
|
||||
Online bool `json:"online"`
|
||||
X *float64 `json:"x,omitempty"`
|
||||
Y *float64 `json:"y,omitempty"`
|
||||
Z *float64 `json:"z,omitempty"`
|
||||
Position *SCUMPositionBody `json:"position,omitempty"`
|
||||
BankBalance *int64 `json:"bankBalance,omitempty"`
|
||||
NormalBalance *int64 `json:"normalBalance,omitempty"`
|
||||
GoldBars *int64 `json:"goldBars,omitempty"`
|
||||
GoldBalance *int64 `json:"goldBalance,omitempty"`
|
||||
RiddenVehicleID string `json:"riddenVehicleId,omitempty"`
|
||||
GameVehicleID string `json:"gameVehicleId,omitempty"`
|
||||
Source string `json:"source"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
type SCUMUserListResponse struct {
|
||||
Items []SCUMUserResponse `json:"items"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
type SCUMUserTrajectoryResponse struct {
|
||||
ID string `json:"id"`
|
||||
ServerInstanceID string `json:"serverInstanceId"`
|
||||
SCUMUserID string `json:"scumUserId"`
|
||||
SubjectType string `json:"subjectType"`
|
||||
SubjectID string `json:"subjectId"`
|
||||
SteamID string `json:"steamId"`
|
||||
DisplayName string `json:"displayName,omitempty"`
|
||||
X float64 `json:"x"`
|
||||
Y float64 `json:"y"`
|
||||
Z float64 `json:"z"`
|
||||
RiddenVehicleID string `json:"riddenVehicleId,omitempty"`
|
||||
GameVehicleID string `json:"gameVehicleId,omitempty"`
|
||||
ObservedAt time.Time `json:"observedAt"`
|
||||
SampledAt time.Time `json:"sampledAt"`
|
||||
Source string `json:"source"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
type SCUMUserTrajectoryListResponse struct {
|
||||
Items []SCUMUserTrajectoryResponse `json:"items"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
type SCUMVehicleResponse struct {
|
||||
ID string `json:"id"`
|
||||
ServerInstanceID string `json:"serverInstanceId"`
|
||||
VehicleID string `json:"vehicleId"`
|
||||
GameVehicleID string `json:"gameVehicleId"`
|
||||
EntityID string `json:"entityId"`
|
||||
VehicleClass string `json:"vehicleClass,omitempty"`
|
||||
ClassName string `json:"className,omitempty"`
|
||||
DisplayName string `json:"displayName,omitempty"`
|
||||
Label string `json:"label,omitempty"`
|
||||
Exists bool `json:"exists"`
|
||||
Status string `json:"status"`
|
||||
Locked bool `json:"locked"`
|
||||
X *float64 `json:"x,omitempty"`
|
||||
Y *float64 `json:"y,omitempty"`
|
||||
Z *float64 `json:"z,omitempty"`
|
||||
Position *SCUMPositionBody `json:"position,omitempty"`
|
||||
LastObservedAt time.Time `json:"lastObservedAt"`
|
||||
VehicleObservedAt time.Time `json:"vehicleObservedAt"`
|
||||
Source string `json:"source"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
type SCUMVehicleListResponse struct {
|
||||
Items []SCUMVehicleResponse `json:"items"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
type SCUMVehicleTrajectoryResponse struct {
|
||||
ID string `json:"id"`
|
||||
ServerInstanceID string `json:"serverInstanceId"`
|
||||
SCUMVehicleID string `json:"scumVehicleId"`
|
||||
SubjectType string `json:"subjectType"`
|
||||
SubjectID string `json:"subjectId"`
|
||||
VehicleID string `json:"vehicleId"`
|
||||
GameVehicleID string `json:"gameVehicleId"`
|
||||
VehicleClass string `json:"vehicleClass,omitempty"`
|
||||
ClassName string `json:"className,omitempty"`
|
||||
X float64 `json:"x"`
|
||||
Y float64 `json:"y"`
|
||||
Z float64 `json:"z"`
|
||||
ObservedAt time.Time `json:"observedAt"`
|
||||
SampledAt time.Time `json:"sampledAt"`
|
||||
Source string `json:"source"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
type SCUMVehicleTrajectoryListResponse struct {
|
||||
Items []SCUMVehicleTrajectoryResponse `json:"items"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
type SCUMVehicleLockResponse struct {
|
||||
ID string `json:"id"`
|
||||
ServerInstanceID string `json:"serverInstanceId"`
|
||||
SCUMVehicleID string `json:"scumVehicleId"`
|
||||
GameVehicleID string `json:"gameVehicleId"`
|
||||
SCUMUserID string `json:"scumUserId"`
|
||||
SteamID string `json:"steamId"`
|
||||
LockedAt time.Time `json:"lockedAt"`
|
||||
Source string `json:"source"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
type SCUMVehicleLockListResponse struct {
|
||||
Items []SCUMVehicleLockResponse `json:"items"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
type SCUMFactIngestRequest struct {
|
||||
RunEndpointID string `json:"runEndpointId"`
|
||||
SessionToken string `json:"sessionToken"`
|
||||
ServerInstanceID string `json:"serverInstanceId"`
|
||||
Users []SCUMUserFactBody `json:"users,omitempty"`
|
||||
Vehicles []SCUMVehicleFactBody `json:"vehicles,omitempty"`
|
||||
}
|
||||
|
||||
type SCUMUserFactBody struct {
|
||||
SteamID string `json:"steamId"`
|
||||
DisplayName string `json:"displayName,omitempty"`
|
||||
SquadID *int64 `json:"squadId,omitempty"`
|
||||
LoginIP string `json:"loginIp,omitempty"`
|
||||
Online bool `json:"online"`
|
||||
Login bool `json:"login,omitempty"`
|
||||
ObservedAt time.Time `json:"observedAt,omitempty"`
|
||||
LoginObservedAt time.Time `json:"loginObservedAt,omitempty"`
|
||||
Position *SCUMPositionBody `json:"position,omitempty"`
|
||||
BankBalance *int64 `json:"bankBalance,omitempty"`
|
||||
GoldBars *int64 `json:"goldBars,omitempty"`
|
||||
RiddenGameVehicleID string `json:"riddenGameVehicleId,omitempty"`
|
||||
}
|
||||
|
||||
type SCUMVehicleFactBody struct {
|
||||
GameVehicleID string `json:"gameVehicleId"`
|
||||
VehicleClass string `json:"vehicleClass,omitempty"`
|
||||
DisplayName string `json:"displayName,omitempty"`
|
||||
Exists *bool `json:"exists,omitempty"`
|
||||
Locked *bool `json:"locked,omitempty"`
|
||||
LockedBySteamID string `json:"lockedBySteamId,omitempty"`
|
||||
LockedAt time.Time `json:"lockedAt,omitempty"`
|
||||
ObservedAt time.Time `json:"observedAt,omitempty"`
|
||||
Position *SCUMPositionBody `json:"position,omitempty"`
|
||||
}
|
||||
|
||||
type SCUMFactIngestResponse struct {
|
||||
Accepted bool `json:"accepted"`
|
||||
AcceptedUserCount int `json:"acceptedUserCount"`
|
||||
AcceptedVehicleCount int `json:"acceptedVehicleCount"`
|
||||
WelcomeQueuedCount int `json:"welcomeQueuedCount"`
|
||||
ServerTime time.Time `json:"serverTime"`
|
||||
}
|
||||
|
||||
func SCUMUserListFromDomain(values []domain.SCUMUser) SCUMUserListResponse {
|
||||
items := make([]SCUMUserResponse, len(values))
|
||||
for i, value := range values {
|
||||
items[i] = SCUMUserFromDomain(value)
|
||||
}
|
||||
return SCUMUserListResponse{Items: items, Count: len(items)}
|
||||
}
|
||||
|
||||
func SCUMUserFromDomain(value domain.SCUMUser) SCUMUserResponse {
|
||||
value = domain.CopySCUMUser(value)
|
||||
return SCUMUserResponse{ID: value.ID, ServerInstanceID: value.ServerInstanceID, SteamID: value.SteamID, GamePlayerID: value.SteamID, DisplayName: value.DisplayName, SquadID: value.SquadID, RegisteredAt: value.RegisteredAt, LastLoginAt: value.LastLoginAt, LastSeenAt: value.LastActivityAt, LastActivityAt: value.LastActivityAt, LastLoginAgeSeconds: int64(value.LastLoginAge.Seconds()), LastLoginIP: value.LastLoginIP, LoginIP: value.LastLoginIP, Online: value.Online, X: value.X, Y: value.Y, Z: value.Z, Position: positionFromPointers(value.X, value.Y, value.Z), BankBalance: value.BankBalance, NormalBalance: value.BankBalance, GoldBars: value.GoldBars, GoldBalance: value.GoldBars, RiddenVehicleID: value.RiddenVehicleID, GameVehicleID: value.GameVehicleID, Source: "platform.scum_user", CreatedAt: value.CreatedAt, UpdatedAt: value.UpdatedAt}
|
||||
}
|
||||
|
||||
func SCUMUserTrajectoryListFromDomain(values []domain.SCUMUserTrajectory) SCUMUserTrajectoryListResponse {
|
||||
items := make([]SCUMUserTrajectoryResponse, len(values))
|
||||
for i, value := range values {
|
||||
items[i] = SCUMUserTrajectoryFromDomain(value)
|
||||
}
|
||||
return SCUMUserTrajectoryListResponse{Items: items, Count: len(items)}
|
||||
}
|
||||
|
||||
func SCUMUserTrajectoryFromDomain(value domain.SCUMUserTrajectory) SCUMUserTrajectoryResponse {
|
||||
return SCUMUserTrajectoryResponse{ID: value.ID, ServerInstanceID: value.ServerInstanceID, SCUMUserID: value.SCUMUserID, SubjectType: "player", SubjectID: value.SteamID, SteamID: value.SteamID, DisplayName: value.DisplayName, X: value.X, Y: value.Y, Z: value.Z, RiddenVehicleID: value.RiddenVehicleID, GameVehicleID: value.GameVehicleID, ObservedAt: value.ObservedAt, SampledAt: value.ObservedAt, Source: "platform.scum_user_trajectory", CreatedAt: value.CreatedAt}
|
||||
}
|
||||
|
||||
func SCUMVehicleListFromDomain(values []domain.SCUMVehicle) SCUMVehicleListResponse {
|
||||
items := make([]SCUMVehicleResponse, len(values))
|
||||
for i, value := range values {
|
||||
items[i] = SCUMVehicleFromDomain(value)
|
||||
}
|
||||
return SCUMVehicleListResponse{Items: items, Count: len(items)}
|
||||
}
|
||||
|
||||
func SCUMVehicleFromDomain(value domain.SCUMVehicle) SCUMVehicleResponse {
|
||||
value = domain.CopySCUMVehicle(value)
|
||||
status := "missing"
|
||||
if value.Exists {
|
||||
status = "exists"
|
||||
}
|
||||
return SCUMVehicleResponse{ID: value.ID, ServerInstanceID: value.ServerInstanceID, VehicleID: value.ID, GameVehicleID: value.GameVehicleID, EntityID: value.GameVehicleID, VehicleClass: value.VehicleClass, ClassName: value.VehicleClass, DisplayName: value.DisplayName, Label: value.DisplayName, Exists: value.Exists, Status: status, Locked: value.Locked, X: value.X, Y: value.Y, Z: value.Z, Position: positionFromPointers(value.X, value.Y, value.Z), LastObservedAt: value.LastObservedAt, VehicleObservedAt: value.LastObservedAt, Source: "platform.scum_vehicle", CreatedAt: value.CreatedAt, UpdatedAt: value.UpdatedAt}
|
||||
}
|
||||
|
||||
func SCUMVehicleTrajectoryListFromDomain(values []domain.SCUMVehicleTrajectory) SCUMVehicleTrajectoryListResponse {
|
||||
items := make([]SCUMVehicleTrajectoryResponse, len(values))
|
||||
for i, value := range values {
|
||||
items[i] = SCUMVehicleTrajectoryFromDomain(value)
|
||||
}
|
||||
return SCUMVehicleTrajectoryListResponse{Items: items, Count: len(items)}
|
||||
}
|
||||
|
||||
func SCUMVehicleTrajectoryFromDomain(value domain.SCUMVehicleTrajectory) SCUMVehicleTrajectoryResponse {
|
||||
return SCUMVehicleTrajectoryResponse{ID: value.ID, ServerInstanceID: value.ServerInstanceID, SCUMVehicleID: value.SCUMVehicleID, SubjectType: "vehicle", SubjectID: value.GameVehicleID, VehicleID: value.SCUMVehicleID, GameVehicleID: value.GameVehicleID, VehicleClass: value.VehicleClass, ClassName: value.VehicleClass, X: value.X, Y: value.Y, Z: value.Z, ObservedAt: value.ObservedAt, SampledAt: value.ObservedAt, Source: "platform.scum_vehicle_trajectory", CreatedAt: value.CreatedAt}
|
||||
}
|
||||
|
||||
func SCUMVehicleLockListFromDomain(values []domain.SCUMVehicleLock) SCUMVehicleLockListResponse {
|
||||
items := make([]SCUMVehicleLockResponse, len(values))
|
||||
for i, value := range values {
|
||||
items[i] = SCUMVehicleLockFromDomain(value)
|
||||
}
|
||||
return SCUMVehicleLockListResponse{Items: items, Count: len(items)}
|
||||
}
|
||||
|
||||
func SCUMVehicleLockFromDomain(value domain.SCUMVehicleLock) SCUMVehicleLockResponse {
|
||||
return SCUMVehicleLockResponse{ID: value.ID, ServerInstanceID: value.ServerInstanceID, SCUMVehicleID: value.SCUMVehicleID, GameVehicleID: value.GameVehicleID, SCUMUserID: value.SCUMUserID, SteamID: value.SteamID, LockedAt: value.LockedAt, Source: "platform.scum_vehicle_lock", CreatedAt: value.CreatedAt}
|
||||
}
|
||||
|
||||
func (request SCUMFactIngestRequest) ToDomain() domain.SCUMFactIngest {
|
||||
users := make([]domain.SCUMUserFact, len(request.Users))
|
||||
for i, user := range request.Users {
|
||||
users[i] = user.ToDomain()
|
||||
}
|
||||
vehicles := make([]domain.SCUMVehicleFact, len(request.Vehicles))
|
||||
for i, vehicle := range request.Vehicles {
|
||||
vehicles[i] = vehicle.ToDomain()
|
||||
}
|
||||
return domain.SCUMFactIngest{RunEndpointID: request.RunEndpointID, SessionToken: request.SessionToken, ServerInstanceID: request.ServerInstanceID, Users: users, Vehicles: vehicles}
|
||||
}
|
||||
|
||||
func (request SCUMUserFactBody) ToDomain() domain.SCUMUserFact {
|
||||
return domain.SCUMUserFact{SteamID: request.SteamID, DisplayName: request.DisplayName, SquadID: request.SquadID, LoginIP: request.LoginIP, Online: request.Online, Login: request.Login, ObservedAt: request.ObservedAt, LoginObservedAt: request.LoginObservedAt, Position: positionToDomain(request.Position), BankBalance: request.BankBalance, GoldBars: request.GoldBars, RiddenGameVehicleID: request.RiddenGameVehicleID}
|
||||
}
|
||||
|
||||
func (request SCUMVehicleFactBody) ToDomain() domain.SCUMVehicleFact {
|
||||
return domain.SCUMVehicleFact{GameVehicleID: request.GameVehicleID, VehicleClass: request.VehicleClass, DisplayName: request.DisplayName, Exists: request.Exists, Locked: request.Locked, LockedBySteamID: request.LockedBySteamID, LockedAt: request.LockedAt, ObservedAt: request.ObservedAt, Position: positionToDomain(request.Position)}
|
||||
}
|
||||
|
||||
func SCUMFactIngestFromDomain(result domain.SCUMFactIngestResult) SCUMFactIngestResponse {
|
||||
return SCUMFactIngestResponse{Accepted: result.Accepted, AcceptedUserCount: result.AcceptedUserCount, AcceptedVehicleCount: result.AcceptedVehicleCount, WelcomeQueuedCount: result.WelcomeQueuedCount, ServerTime: result.ServerTime}
|
||||
}
|
||||
|
||||
func positionToDomain(value *SCUMPositionBody) *domain.SCUMPosition {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
return &domain.SCUMPosition{X: value.X, Y: value.Y, Z: value.Z}
|
||||
}
|
||||
|
||||
func positionFromPointers(x, y, z *float64) *SCUMPositionBody {
|
||||
if x == nil || y == nil || z == nil {
|
||||
return nil
|
||||
}
|
||||
return &SCUMPositionBody{X: *x, Y: *y, Z: *z}
|
||||
}
|
||||
@@ -19,6 +19,11 @@ func TestTableNames(t *testing.T) {
|
||||
LogStream{}.TableName(): "log_streams",
|
||||
PluginLifecycleInstallation{}.TableName(): "plugin_lifecycle_installations",
|
||||
AIConfigDiff{}.TableName(): "ai_config_diffs",
|
||||
SCUMUser{}.TableName(): "scum_user",
|
||||
SCUMUserTrajectory{}.TableName(): "scum_user_trajectory",
|
||||
SCUMVehicle{}.TableName(): "scum_vehicle",
|
||||
SCUMVehicleTrajectory{}.TableName(): "scum_vehicle_trajectory",
|
||||
SCUMVehicleLock{}.TableName(): "scum_vehicle_lock",
|
||||
}
|
||||
|
||||
for got, want := range tests {
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type SCUMUser struct {
|
||||
// ID is the stable platform SCUM user row identifier.
|
||||
ID string `json:"id" db:"id"`
|
||||
// ServerInstanceID scopes the record to one platform server instance.
|
||||
ServerInstanceID string `json:"serverInstanceId" db:"server_instance_id"`
|
||||
// SteamID stores the user's Steam identifier and is indexed with the server.
|
||||
SteamID string `json:"steamId" db:"steam_id"`
|
||||
// DisplayName is the latest observed in-game display name.
|
||||
DisplayName string `json:"displayName,omitempty" db:"display_name"`
|
||||
// SquadID stores the latest observed SCUM squad identifier.
|
||||
SquadID int64 `json:"squadId,omitempty" db:"squad_id"`
|
||||
// RegisteredAt is when the platform first created this SCUM user record.
|
||||
RegisteredAt time.Time `json:"registeredAt" db:"registered_at"`
|
||||
// LastLoginAt is the latest login timestamp derived from Run-pushed facts.
|
||||
LastLoginAt time.Time `json:"lastLoginAt" db:"last_login_at"`
|
||||
// LastActivityAt is the latest user activity or observed online timestamp.
|
||||
LastActivityAt time.Time `json:"lastActivityAt" db:"last_activity_at"`
|
||||
// LastLoginIP stores the latest login IP reported by plugin-owned facts.
|
||||
LastLoginIP string `json:"lastLoginIp,omitempty" db:"last_login_ip"`
|
||||
// Online marks whether the user is currently considered online by platform facts.
|
||||
Online bool `json:"online" db:"online"`
|
||||
// X stores the latest observed world X coordinate.
|
||||
X *float64 `json:"x,omitempty" db:"x"`
|
||||
// Y stores the latest observed world Y coordinate.
|
||||
Y *float64 `json:"y,omitempty" db:"y"`
|
||||
// Z stores the latest observed world Z coordinate.
|
||||
Z *float64 `json:"z,omitempty" db:"z"`
|
||||
// BankBalance stores the latest observed bank scum-bucks balance.
|
||||
BankBalance *int64 `json:"bankBalance,omitempty" db:"bank_balance"`
|
||||
// GoldBars stores the latest observed gold-bar count.
|
||||
GoldBars *int64 `json:"goldBars,omitempty" db:"gold_bars"`
|
||||
// RiddenVehicleID links to the platform SCUM vehicle row when mounted.
|
||||
RiddenVehicleID string `json:"riddenVehicleId,omitempty" db:"ridden_vehicle_id"`
|
||||
// GameVehicleID stores the in-game vehicle identifier when mounted.
|
||||
GameVehicleID string `json:"gameVehicleId,omitempty" db:"game_vehicle_id"`
|
||||
// WelcomeQueuedAt records the delayed public welcome job eligibility time.
|
||||
WelcomeQueuedAt time.Time `json:"welcomeQueuedAt,omitempty" db:"welcome_queued_at"`
|
||||
// WelcomeLastError stores the latest welcome-message queue failure.
|
||||
WelcomeLastError string `json:"welcomeLastError,omitempty" db:"welcome_last_error"`
|
||||
// CreatedAt is the platform row creation timestamp.
|
||||
CreatedAt time.Time `json:"createdAt" db:"created_at"`
|
||||
// UpdatedAt is the latest platform row update timestamp.
|
||||
UpdatedAt time.Time `json:"updatedAt" db:"updated_at"`
|
||||
}
|
||||
|
||||
func (SCUMUser) TableName() string { return "scum_user" }
|
||||
|
||||
type SCUMUserTrajectory struct {
|
||||
// ID is the stable platform trajectory sample identifier.
|
||||
ID string `json:"id" db:"id"`
|
||||
// ServerInstanceID scopes the sample to one platform server instance.
|
||||
ServerInstanceID string `json:"serverInstanceId" db:"server_instance_id"`
|
||||
// SCUMUserID links to the platform SCUM user row.
|
||||
SCUMUserID string `json:"scumUserId" db:"scum_user_id"`
|
||||
// SteamID stores the user's Steam identifier for indexed lookups.
|
||||
SteamID string `json:"steamId" db:"steam_id"`
|
||||
// DisplayName stores the name observed with this sample.
|
||||
DisplayName string `json:"displayName,omitempty" db:"display_name"`
|
||||
// X stores the sampled world X coordinate.
|
||||
X float64 `json:"x" db:"x"`
|
||||
// Y stores the sampled world Y coordinate.
|
||||
Y float64 `json:"y" db:"y"`
|
||||
// Z stores the sampled world Z coordinate.
|
||||
Z float64 `json:"z" db:"z"`
|
||||
// RiddenVehicleID links to the platform vehicle row when mounted.
|
||||
RiddenVehicleID string `json:"riddenVehicleId,omitempty" db:"ridden_vehicle_id"`
|
||||
// GameVehicleID stores the in-game vehicle identifier when mounted.
|
||||
GameVehicleID string `json:"gameVehicleId,omitempty" db:"game_vehicle_id"`
|
||||
// ObservedAt is when the position fact was observed.
|
||||
ObservedAt time.Time `json:"observedAt" db:"observed_at"`
|
||||
// CreatedAt is the platform row creation timestamp.
|
||||
CreatedAt time.Time `json:"createdAt" db:"created_at"`
|
||||
}
|
||||
|
||||
func (SCUMUserTrajectory) TableName() string { return "scum_user_trajectory" }
|
||||
|
||||
type SCUMVehicle struct {
|
||||
// ID is the stable platform SCUM vehicle row identifier.
|
||||
ID string `json:"id" db:"id"`
|
||||
// ServerInstanceID scopes the record to one platform server instance.
|
||||
ServerInstanceID string `json:"serverInstanceId" db:"server_instance_id"`
|
||||
// GameVehicleID stores the in-game vehicle identifier.
|
||||
GameVehicleID string `json:"gameVehicleId" db:"game_vehicle_id"`
|
||||
// VehicleClass stores the latest observed SCUM vehicle class.
|
||||
VehicleClass string `json:"vehicleClass,omitempty" db:"vehicle_class"`
|
||||
// DisplayName is the user-visible vehicle label.
|
||||
DisplayName string `json:"displayName,omitempty" db:"display_name"`
|
||||
// Exists marks whether the vehicle still exists in game.
|
||||
Exists bool `json:"exists" db:"exists"`
|
||||
// Locked marks the latest observed lock state.
|
||||
Locked bool `json:"locked" db:"locked"`
|
||||
// X stores the latest observed world X coordinate.
|
||||
X *float64 `json:"x,omitempty" db:"x"`
|
||||
// Y stores the latest observed world Y coordinate.
|
||||
Y *float64 `json:"y,omitempty" db:"y"`
|
||||
// Z stores the latest observed world Z coordinate.
|
||||
Z *float64 `json:"z,omitempty" db:"z"`
|
||||
// LastObservedAt is when the latest vehicle fact was observed.
|
||||
LastObservedAt time.Time `json:"lastObservedAt" db:"last_observed_at"`
|
||||
// CreatedAt is the platform row creation timestamp.
|
||||
CreatedAt time.Time `json:"createdAt" db:"created_at"`
|
||||
// UpdatedAt is the latest platform row update timestamp.
|
||||
UpdatedAt time.Time `json:"updatedAt" db:"updated_at"`
|
||||
}
|
||||
|
||||
func (SCUMVehicle) TableName() string { return "scum_vehicle" }
|
||||
|
||||
type SCUMVehicleTrajectory struct {
|
||||
// ID is the stable platform vehicle trajectory sample identifier.
|
||||
ID string `json:"id" db:"id"`
|
||||
// ServerInstanceID scopes the sample to one platform server instance.
|
||||
ServerInstanceID string `json:"serverInstanceId" db:"server_instance_id"`
|
||||
// SCUMVehicleID links to the platform SCUM vehicle row.
|
||||
SCUMVehicleID string `json:"scumVehicleId" db:"scum_vehicle_id"`
|
||||
// GameVehicleID stores the in-game vehicle identifier.
|
||||
GameVehicleID string `json:"gameVehicleId" db:"game_vehicle_id"`
|
||||
// VehicleClass stores the vehicle class observed with this sample.
|
||||
VehicleClass string `json:"vehicleClass,omitempty" db:"vehicle_class"`
|
||||
// X stores the sampled world X coordinate.
|
||||
X float64 `json:"x" db:"x"`
|
||||
// Y stores the sampled world Y coordinate.
|
||||
Y float64 `json:"y" db:"y"`
|
||||
// Z stores the sampled world Z coordinate.
|
||||
Z float64 `json:"z" db:"z"`
|
||||
// ObservedAt is when the position fact was observed.
|
||||
ObservedAt time.Time `json:"observedAt" db:"observed_at"`
|
||||
// CreatedAt is the platform row creation timestamp.
|
||||
CreatedAt time.Time `json:"createdAt" db:"created_at"`
|
||||
}
|
||||
|
||||
func (SCUMVehicleTrajectory) TableName() string { return "scum_vehicle_trajectory" }
|
||||
|
||||
type SCUMVehicleLock struct {
|
||||
// ID is the stable platform vehicle-lock history row identifier.
|
||||
ID string `json:"id" db:"id"`
|
||||
// ServerInstanceID scopes the lock event to one platform server instance.
|
||||
ServerInstanceID string `json:"serverInstanceId" db:"server_instance_id"`
|
||||
// SCUMVehicleID links to the platform SCUM vehicle row.
|
||||
SCUMVehicleID string `json:"scumVehicleId" db:"scum_vehicle_id"`
|
||||
// GameVehicleID stores the in-game vehicle identifier.
|
||||
GameVehicleID string `json:"gameVehicleId" db:"game_vehicle_id"`
|
||||
// SCUMUserID links to the platform SCUM user row that applied the lock.
|
||||
SCUMUserID string `json:"scumUserId" db:"scum_user_id"`
|
||||
// SteamID stores the locking user's Steam identifier.
|
||||
SteamID string `json:"steamId" db:"steam_id"`
|
||||
// LockedAt is when the vehicle lock was observed or applied.
|
||||
LockedAt time.Time `json:"lockedAt" db:"locked_at"`
|
||||
// CreatedAt is the platform row creation timestamp.
|
||||
CreatedAt time.Time `json:"createdAt" db:"created_at"`
|
||||
}
|
||||
|
||||
func (SCUMVehicleLock) TableName() string { return "scum_vehicle_lock" }
|
||||
@@ -36,6 +36,11 @@ type StoreSnapshot struct {
|
||||
GameClientBridgeSnapshots []domain.GameClientBridgeSnapshot `json:"gameClientBridgeSnapshots"`
|
||||
GameClientBridgeStreams []domain.GameClientBridgeSnapshotStream `json:"gameClientBridgeStreams"`
|
||||
PluginDataRecords []domain.PluginDataRecord `json:"pluginDataRecords"`
|
||||
SCUMUsers []domain.SCUMUser `json:"scumUsers"`
|
||||
SCUMUserTrajectories []domain.SCUMUserTrajectory `json:"scumUserTrajectories"`
|
||||
SCUMVehicles []domain.SCUMVehicle `json:"scumVehicles"`
|
||||
SCUMVehicleTrajectories []domain.SCUMVehicleTrajectory `json:"scumVehicleTrajectories"`
|
||||
SCUMVehicleLocks []domain.SCUMVehicleLock `json:"scumVehicleLocks"`
|
||||
}
|
||||
|
||||
type runtimeSnapshot struct {
|
||||
@@ -168,6 +173,21 @@ func (store *FileStore) GameClientBridgeSnapshotStreams() GameClientBridgeSnapsh
|
||||
func (store *FileStore) PluginDataRecords() PluginDataRecordRepository {
|
||||
return &persistentRepository[domain.PluginDataRecord, domain.PluginDataFilter]{repository: store.MemoryStore.pluginDataRecords, persist: store.persist}
|
||||
}
|
||||
func (store *FileStore) SCUMUsers() SCUMUserRepository {
|
||||
return &persistentRepository[domain.SCUMUser, domain.SCUMUserFilter]{repository: store.MemoryStore.scumUsers, persist: store.persist}
|
||||
}
|
||||
func (store *FileStore) SCUMUserTrajectories() SCUMUserTrajectoryRepository {
|
||||
return &persistentRepository[domain.SCUMUserTrajectory, domain.SCUMUserTrajectoryFilter]{repository: store.MemoryStore.scumUserTracks, persist: store.persist}
|
||||
}
|
||||
func (store *FileStore) SCUMVehicles() SCUMVehicleRepository {
|
||||
return &persistentRepository[domain.SCUMVehicle, domain.SCUMVehicleFilter]{repository: store.MemoryStore.scumVehicles, persist: store.persist}
|
||||
}
|
||||
func (store *FileStore) SCUMVehicleTrajectories() SCUMVehicleTrajectoryRepository {
|
||||
return &persistentRepository[domain.SCUMVehicleTrajectory, domain.SCUMVehicleTrajectoryFilter]{repository: store.MemoryStore.scumVehicleTracks, persist: store.persist}
|
||||
}
|
||||
func (store *FileStore) SCUMVehicleLocks() SCUMVehicleLockRepository {
|
||||
return &persistentRepository[domain.SCUMVehicleLock, domain.SCUMVehicleLockFilter]{repository: store.MemoryStore.scumVehicleLocks, persist: store.persist}
|
||||
}
|
||||
func (store *FileStore) load() error {
|
||||
data, err := os.ReadFile(store.path)
|
||||
if err != nil {
|
||||
@@ -312,6 +332,11 @@ func (store *FileStore) snapshot() StoreSnapshot {
|
||||
GameClientBridgeSnapshots: snapshotRepository(store.MemoryStore.bridgeSnapshots.memoryRepository),
|
||||
GameClientBridgeStreams: snapshotRepository(store.MemoryStore.bridgeStreams),
|
||||
PluginDataRecords: snapshotRepository(store.MemoryStore.pluginDataRecords),
|
||||
SCUMUsers: snapshotRepository(store.MemoryStore.scumUsers),
|
||||
SCUMUserTrajectories: snapshotRepository(store.MemoryStore.scumUserTracks),
|
||||
SCUMVehicles: snapshotRepository(store.MemoryStore.scumVehicles),
|
||||
SCUMVehicleTrajectories: snapshotRepository(store.MemoryStore.scumVehicleTracks),
|
||||
SCUMVehicleLocks: snapshotRepository(store.MemoryStore.scumVehicleLocks),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -347,6 +372,11 @@ func (store *FileStore) loadSnapshot(snapshot StoreSnapshot) {
|
||||
loadRepository(store.MemoryStore.bridgeSnapshots.memoryRepository, snapshot.GameClientBridgeSnapshots)
|
||||
loadRepository(store.MemoryStore.bridgeStreams, snapshot.GameClientBridgeStreams)
|
||||
loadRepository(store.MemoryStore.pluginDataRecords, snapshot.PluginDataRecords)
|
||||
loadRepository(store.MemoryStore.scumUsers, snapshot.SCUMUsers)
|
||||
loadRepository(store.MemoryStore.scumUserTracks, snapshot.SCUMUserTrajectories)
|
||||
loadRepository(store.MemoryStore.scumVehicles, snapshot.SCUMVehicles)
|
||||
loadRepository(store.MemoryStore.scumVehicleTracks, snapshot.SCUMVehicleTrajectories)
|
||||
loadRepository(store.MemoryStore.scumVehicleLocks, snapshot.SCUMVehicleLocks)
|
||||
}
|
||||
|
||||
func (store *FileStore) loadRuntimeSnapshot(snapshot runtimeSnapshot) {
|
||||
|
||||
@@ -150,6 +150,21 @@ func (store *MySQLStore) GameClientBridgeSnapshotStreams() GameClientBridgeSnaps
|
||||
func (store *MySQLStore) PluginDataRecords() PluginDataRecordRepository {
|
||||
return &persistentRepository[domain.PluginDataRecord, domain.PluginDataFilter]{repository: store.MemoryStore.pluginDataRecords, persist: store.persist}
|
||||
}
|
||||
func (store *MySQLStore) SCUMUsers() SCUMUserRepository {
|
||||
return &persistentRepository[domain.SCUMUser, domain.SCUMUserFilter]{repository: store.MemoryStore.scumUsers, persist: store.persist}
|
||||
}
|
||||
func (store *MySQLStore) SCUMUserTrajectories() SCUMUserTrajectoryRepository {
|
||||
return &persistentRepository[domain.SCUMUserTrajectory, domain.SCUMUserTrajectoryFilter]{repository: store.MemoryStore.scumUserTracks, persist: store.persist}
|
||||
}
|
||||
func (store *MySQLStore) SCUMVehicles() SCUMVehicleRepository {
|
||||
return &persistentRepository[domain.SCUMVehicle, domain.SCUMVehicleFilter]{repository: store.MemoryStore.scumVehicles, persist: store.persist}
|
||||
}
|
||||
func (store *MySQLStore) SCUMVehicleTrajectories() SCUMVehicleTrajectoryRepository {
|
||||
return &persistentRepository[domain.SCUMVehicleTrajectory, domain.SCUMVehicleTrajectoryFilter]{repository: store.MemoryStore.scumVehicleTracks, persist: store.persist}
|
||||
}
|
||||
func (store *MySQLStore) SCUMVehicleLocks() SCUMVehicleLockRepository {
|
||||
return &persistentRepository[domain.SCUMVehicleLock, domain.SCUMVehicleLockFilter]{repository: store.MemoryStore.scumVehicleLocks, persist: store.persist}
|
||||
}
|
||||
func (store *MySQLStore) initialize() error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
@@ -273,6 +288,11 @@ func (store *MySQLStore) snapshot() StoreSnapshot {
|
||||
GameClientBridgeSnapshots: snapshotRepository(store.MemoryStore.bridgeSnapshots.memoryRepository),
|
||||
GameClientBridgeStreams: snapshotRepository(store.MemoryStore.bridgeStreams),
|
||||
PluginDataRecords: snapshotRepository(store.MemoryStore.pluginDataRecords),
|
||||
SCUMUsers: snapshotRepository(store.MemoryStore.scumUsers),
|
||||
SCUMUserTrajectories: snapshotRepository(store.MemoryStore.scumUserTracks),
|
||||
SCUMVehicles: snapshotRepository(store.MemoryStore.scumVehicles),
|
||||
SCUMVehicleTrajectories: snapshotRepository(store.MemoryStore.scumVehicleTracks),
|
||||
SCUMVehicleLocks: snapshotRepository(store.MemoryStore.scumVehicleLocks),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,6 +328,11 @@ func (store *MySQLStore) loadSnapshot(snapshot StoreSnapshot) {
|
||||
loadRepository(store.MemoryStore.bridgeSnapshots.memoryRepository, snapshot.GameClientBridgeSnapshots)
|
||||
loadRepository(store.MemoryStore.bridgeStreams, snapshot.GameClientBridgeStreams)
|
||||
loadRepository(store.MemoryStore.pluginDataRecords, snapshot.PluginDataRecords)
|
||||
loadRepository(store.MemoryStore.scumUsers, snapshot.SCUMUsers)
|
||||
loadRepository(store.MemoryStore.scumUserTracks, snapshot.SCUMUserTrajectories)
|
||||
loadRepository(store.MemoryStore.scumVehicles, snapshot.SCUMVehicles)
|
||||
loadRepository(store.MemoryStore.scumVehicleTracks, snapshot.SCUMVehicleTrajectories)
|
||||
loadRepository(store.MemoryStore.scumVehicleLocks, snapshot.SCUMVehicleLocks)
|
||||
}
|
||||
|
||||
func (store *MySQLStore) loadRuntimeSnapshot(snapshot runtimeSnapshot) {
|
||||
|
||||
@@ -184,6 +184,41 @@ type PluginDataRecordRepository interface {
|
||||
Apply([]domain.PluginDataRecord, []string) error
|
||||
}
|
||||
|
||||
type SCUMUserRepository interface {
|
||||
Create(domain.SCUMUser) error
|
||||
Get(string) (domain.SCUMUser, error)
|
||||
List(domain.SCUMUserFilter) ([]domain.SCUMUser, error)
|
||||
Update(domain.SCUMUser) error
|
||||
}
|
||||
|
||||
type SCUMUserTrajectoryRepository interface {
|
||||
Create(domain.SCUMUserTrajectory) error
|
||||
Get(string) (domain.SCUMUserTrajectory, error)
|
||||
List(domain.SCUMUserTrajectoryFilter) ([]domain.SCUMUserTrajectory, error)
|
||||
Update(domain.SCUMUserTrajectory) error
|
||||
}
|
||||
|
||||
type SCUMVehicleRepository interface {
|
||||
Create(domain.SCUMVehicle) error
|
||||
Get(string) (domain.SCUMVehicle, error)
|
||||
List(domain.SCUMVehicleFilter) ([]domain.SCUMVehicle, error)
|
||||
Update(domain.SCUMVehicle) error
|
||||
}
|
||||
|
||||
type SCUMVehicleTrajectoryRepository interface {
|
||||
Create(domain.SCUMVehicleTrajectory) error
|
||||
Get(string) (domain.SCUMVehicleTrajectory, error)
|
||||
List(domain.SCUMVehicleTrajectoryFilter) ([]domain.SCUMVehicleTrajectory, error)
|
||||
Update(domain.SCUMVehicleTrajectory) error
|
||||
}
|
||||
|
||||
type SCUMVehicleLockRepository interface {
|
||||
Create(domain.SCUMVehicleLock) error
|
||||
Get(string) (domain.SCUMVehicleLock, error)
|
||||
List(domain.SCUMVehicleLockFilter) ([]domain.SCUMVehicleLock, error)
|
||||
Update(domain.SCUMVehicleLock) error
|
||||
}
|
||||
|
||||
type Store interface {
|
||||
Users() UserRepository
|
||||
AuthSessions() AuthSessionRepository
|
||||
@@ -208,6 +243,11 @@ type Store interface {
|
||||
GameClientBridgeSnapshots() GameClientBridgeSnapshotRepository
|
||||
GameClientBridgeSnapshotStreams() GameClientBridgeSnapshotStreamRepository
|
||||
PluginDataRecords() PluginDataRecordRepository
|
||||
SCUMUsers() SCUMUserRepository
|
||||
SCUMUserTrajectories() SCUMUserTrajectoryRepository
|
||||
SCUMVehicles() SCUMVehicleRepository
|
||||
SCUMVehicleTrajectories() SCUMVehicleTrajectoryRepository
|
||||
SCUMVehicleLocks() SCUMVehicleLockRepository
|
||||
}
|
||||
|
||||
type MemoryStore struct {
|
||||
@@ -234,6 +274,11 @@ type MemoryStore struct {
|
||||
bridgeSnapshots *memoryGameClientBridgeSnapshotRepository
|
||||
bridgeStreams *memoryRepository[domain.GameClientBridgeSnapshotStream, domain.GameClientBridgeSnapshotStreamFilter]
|
||||
pluginDataRecords *memoryRepository[domain.PluginDataRecord, domain.PluginDataFilter]
|
||||
scumUsers *memoryRepository[domain.SCUMUser, domain.SCUMUserFilter]
|
||||
scumUserTracks *memoryRepository[domain.SCUMUserTrajectory, domain.SCUMUserTrajectoryFilter]
|
||||
scumVehicles *memoryRepository[domain.SCUMVehicle, domain.SCUMVehicleFilter]
|
||||
scumVehicleTracks *memoryRepository[domain.SCUMVehicleTrajectory, domain.SCUMVehicleTrajectoryFilter]
|
||||
scumVehicleLocks *memoryRepository[domain.SCUMVehicleLock, domain.SCUMVehicleLockFilter]
|
||||
}
|
||||
|
||||
func NewMemoryStore() *MemoryStore {
|
||||
@@ -337,6 +382,11 @@ func NewMemoryStore() *MemoryStore {
|
||||
matchGameClientBridgeSnapshotStream,
|
||||
),
|
||||
pluginDataRecords: newMemoryRepository(func(value domain.PluginDataRecord) string { return value.ID }, domain.CopyPluginDataRecord, matchPluginDataRecord),
|
||||
scumUsers: newMemoryRepository(func(value domain.SCUMUser) string { return value.ID }, domain.CopySCUMUser, matchSCUMUser),
|
||||
scumUserTracks: newMemoryRepository(func(value domain.SCUMUserTrajectory) string { return value.ID }, domain.CopySCUMUserTrajectory, matchSCUMUserTrajectory),
|
||||
scumVehicles: newMemoryRepository(func(value domain.SCUMVehicle) string { return value.ID }, domain.CopySCUMVehicle, matchSCUMVehicle),
|
||||
scumVehicleTracks: newMemoryRepository(func(value domain.SCUMVehicleTrajectory) string { return value.ID }, domain.CopySCUMVehicleTrajectory, matchSCUMVehicleTrajectory),
|
||||
scumVehicleLocks: newMemoryRepository(func(value domain.SCUMVehicleLock) string { return value.ID }, domain.CopySCUMVehicleLock, matchSCUMVehicleLock),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -375,6 +425,15 @@ func (store *MemoryStore) GameClientBridgeSnapshotStreams() GameClientBridgeSnap
|
||||
func (store *MemoryStore) PluginDataRecords() PluginDataRecordRepository {
|
||||
return store.pluginDataRecords
|
||||
}
|
||||
func (store *MemoryStore) SCUMUsers() SCUMUserRepository { return store.scumUsers }
|
||||
func (store *MemoryStore) SCUMUserTrajectories() SCUMUserTrajectoryRepository {
|
||||
return store.scumUserTracks
|
||||
}
|
||||
func (store *MemoryStore) SCUMVehicles() SCUMVehicleRepository { return store.scumVehicles }
|
||||
func (store *MemoryStore) SCUMVehicleTrajectories() SCUMVehicleTrajectoryRepository {
|
||||
return store.scumVehicleTracks
|
||||
}
|
||||
func (store *MemoryStore) SCUMVehicleLocks() SCUMVehicleLockRepository { return store.scumVehicleLocks }
|
||||
|
||||
type memoryRepository[T any, F any] struct {
|
||||
mu sync.RWMutex
|
||||
@@ -693,3 +752,40 @@ func matchGameClientBridgeSnapshotStream(stream domain.GameClientBridgeSnapshotS
|
||||
func matchPluginDataRecord(value domain.PluginDataRecord, filter domain.PluginDataFilter) bool {
|
||||
return (filter.PluginID == "" || value.PluginID == filter.PluginID) && (filter.ServerInstanceID == "" || value.ServerInstanceID == filter.ServerInstanceID) && (filter.Collection == "" || value.Collection == filter.Collection) && (filter.Key == "" || value.Key == filter.Key)
|
||||
}
|
||||
|
||||
func matchSCUMUser(value domain.SCUMUser, filter domain.SCUMUserFilter) bool {
|
||||
return (filter.ServerInstanceID == "" || value.ServerInstanceID == filter.ServerInstanceID) &&
|
||||
(filter.SteamID == "" || value.SteamID == filter.SteamID) &&
|
||||
(filter.Online == nil || value.Online == *filter.Online) &&
|
||||
(filter.ChangedAfter.IsZero() || value.UpdatedAt.After(filter.ChangedAfter) || value.LastActivityAt.After(filter.ChangedAfter))
|
||||
}
|
||||
|
||||
func matchSCUMUserTrajectory(value domain.SCUMUserTrajectory, filter domain.SCUMUserTrajectoryFilter) bool {
|
||||
return (filter.ServerInstanceID == "" || value.ServerInstanceID == filter.ServerInstanceID) &&
|
||||
(filter.SCUMUserID == "" || value.SCUMUserID == filter.SCUMUserID) &&
|
||||
(filter.SteamID == "" || value.SteamID == filter.SteamID) &&
|
||||
(filter.After.IsZero() || value.ObservedAt.After(filter.After))
|
||||
}
|
||||
|
||||
func matchSCUMVehicle(value domain.SCUMVehicle, filter domain.SCUMVehicleFilter) bool {
|
||||
return (filter.ServerInstanceID == "" || value.ServerInstanceID == filter.ServerInstanceID) &&
|
||||
(filter.GameVehicleID == "" || value.GameVehicleID == filter.GameVehicleID) &&
|
||||
(filter.Exists == nil || value.Exists == *filter.Exists) &&
|
||||
(filter.ChangedAfter.IsZero() || value.UpdatedAt.After(filter.ChangedAfter) || value.LastObservedAt.After(filter.ChangedAfter))
|
||||
}
|
||||
|
||||
func matchSCUMVehicleTrajectory(value domain.SCUMVehicleTrajectory, filter domain.SCUMVehicleTrajectoryFilter) bool {
|
||||
return (filter.ServerInstanceID == "" || value.ServerInstanceID == filter.ServerInstanceID) &&
|
||||
(filter.SCUMVehicleID == "" || value.SCUMVehicleID == filter.SCUMVehicleID) &&
|
||||
(filter.GameVehicleID == "" || value.GameVehicleID == filter.GameVehicleID) &&
|
||||
(filter.After.IsZero() || value.ObservedAt.After(filter.After))
|
||||
}
|
||||
|
||||
func matchSCUMVehicleLock(value domain.SCUMVehicleLock, filter domain.SCUMVehicleLockFilter) bool {
|
||||
return (filter.ServerInstanceID == "" || value.ServerInstanceID == filter.ServerInstanceID) &&
|
||||
(filter.SCUMVehicleID == "" || value.SCUMVehicleID == filter.SCUMVehicleID) &&
|
||||
(filter.GameVehicleID == "" || value.GameVehicleID == filter.GameVehicleID) &&
|
||||
(filter.SCUMUserID == "" || value.SCUMUserID == filter.SCUMUserID) &&
|
||||
(filter.SteamID == "" || value.SteamID == filter.SteamID) &&
|
||||
(filter.After.IsZero() || value.LockedAt.After(filter.After))
|
||||
}
|
||||
|
||||
@@ -102,15 +102,15 @@ func TestCoreServiceKeepsPlatformBuildKeyOffMachineJobChannel(t *testing.T) {
|
||||
}
|
||||
plugin.RuntimeProfiles.LogSources = append(plugin.RuntimeProfiles.LogSources,
|
||||
domain.RuntimeLogSource{Key: "console", Kind: "process.stdout", TargetKey: "server/process", StreamKey: "console", CursorKind: "sequence", RetentionDays: 14},
|
||||
domain.RuntimeLogSource{Key: "server-events", Kind: "file.tail", TargetKey: "logs/server", StreamKey: "scum.server", CursorKind: "fingerprint", RetentionDays: 90},
|
||||
domain.RuntimeLogSource{Key: "server-events", Kind: "file.tail", TargetKey: "logs/server", StreamKey: "game.server", CursorKind: "fingerprint", RetentionDays: 90},
|
||||
)
|
||||
plugin.RuntimeProfiles.DataTargets = []domain.RuntimeDataTarget{{
|
||||
Key: "scum-database",
|
||||
Key: "game-database",
|
||||
Kind: "sqlite.snapshot",
|
||||
TransportKey: "scum-database",
|
||||
TransportKey: "game-database",
|
||||
SourceRootKey: "server-root",
|
||||
SourcePath: "SCUM/Saved/SaveFiles/SCUM.db",
|
||||
WorkspaceKey: "databases/scum-database",
|
||||
SourcePath: "Game/Saved/SaveFiles/state.db",
|
||||
WorkspaceKey: "databases/game-database",
|
||||
RefreshPolicy: "on-demand-snapshot",
|
||||
MaxBytes: 1024 * 1024,
|
||||
Platforms: []string{"linux"},
|
||||
@@ -118,7 +118,7 @@ func TestCoreServiceKeepsPlatformBuildKeyOffMachineJobChannel(t *testing.T) {
|
||||
if err := svc.store.GamePlugins().Update(plugin); err != nil {
|
||||
t.Fatalf("seed plugin lifecycle assets: %v", err)
|
||||
}
|
||||
instance.Deployment.ServerRoot = "/srv/scum"
|
||||
instance.Deployment.ServerRoot = "/srv/game"
|
||||
if err := svc.store.ServerInstances().Update(instance); err != nil {
|
||||
t.Fatalf("seed instance deployment root: %v", err)
|
||||
}
|
||||
@@ -167,10 +167,10 @@ func TestCoreServiceKeepsPlatformBuildKeyOffMachineJobChannel(t *testing.T) {
|
||||
if len(plan.DependencyProbes) != 1 || plan.DependencyProbes[0].Key != "java-runtime" || len(plan.InstallPlans) != 1 || plan.InstallPlans[0].Key != "java-install" || len(plan.LogSources) != 1 || !hasAutonomousLogSource(plan.LogSources, "process.stdout", "console") || plan.RuntimeBindings["logs/latest"] != "runtime.logs.latest" {
|
||||
t.Fatalf("autonomous lifecycle plan lost plugin runtime declarations: %+v", plan)
|
||||
}
|
||||
if len(plan.DataTargets) != 1 || plan.DataTargets[0].Key != "scum-database" || plan.DataTargets[0].Kind != "sqlite.snapshot" || plan.DataTargets[0].SourcePath != "SCUM/Saved/SaveFiles/SCUM.db" || plan.RuntimeBindings["server-root"] != instance.Deployment.ServerRoot {
|
||||
if len(plan.DataTargets) != 1 || plan.DataTargets[0].Key != "game-database" || plan.DataTargets[0].Kind != "sqlite.snapshot" || plan.DataTargets[0].SourcePath != "Game/Saved/SaveFiles/state.db" || plan.RuntimeBindings["server-root"] != instance.Deployment.ServerRoot {
|
||||
t.Fatalf("autonomous lifecycle plan lost private database target bindings: %+v", plan)
|
||||
}
|
||||
if hasAutonomousLogSource(plan.LogSources, "file.tail", "latest-log") || hasAutonomousLogSource(plan.LogSources, "file.tail", "scum.server") {
|
||||
if hasAutonomousLogSource(plan.LogSources, "file.tail", "latest-log") || hasAutonomousLogSource(plan.LogSources, "file.tail", "game.server") {
|
||||
t.Fatalf("autonomous lifecycle plan must not carry file-tail sources into process.start: %+v", plan.LogSources)
|
||||
}
|
||||
var seededPlan domain.RunAutonomousLifecyclePlan
|
||||
|
||||
@@ -11,18 +11,18 @@ func TestPluginDataIsOpaqueAndScopedToItsServerPlugin(t *testing.T) {
|
||||
plugin, endpoint := createPluginAndRunEndpoint(t, svc)
|
||||
ownerID := "plugin-data-owner"
|
||||
sessionID := createServiceUserAndLogin(t, svc, domain.User{ID: ownerID, DisplayName: "Plugin data owner", Email: "plugin-data@example.test", Roles: []string{"server-owner"}, PasswordHash: "secret-password"})
|
||||
if _, err := svc.CreateServerInstance(domain.ServerInstance{ID: "server-1", PluginID: plugin.ID, RunEndpointID: endpoint.ID, OwnerUserID: ownerID, Name: "SCUM"}); err != nil {
|
||||
if _, err := svc.CreateServerInstance(domain.ServerInstance{ID: "server-1", PluginID: plugin.ID, RunEndpointID: endpoint.ID, OwnerUserID: ownerID, Name: "Game Server"}); err != nil {
|
||||
t.Fatalf("create server: %v", err)
|
||||
}
|
||||
stored, err := svc.PutPluginDataForSession(sessionID, domain.PluginDataRecord{PluginID: "server.scum", ServerInstanceID: "server-1", Collection: "scum_users", Key: "steam-1", Value: map[string]any{"steamId": "steam-1", "futureField": true}})
|
||||
stored, err := svc.PutPluginDataForSession(sessionID, domain.PluginDataRecord{PluginID: "server.scum", ServerInstanceID: "server-1", Collection: "plugin_users", Key: "steam-1", Value: map[string]any{"steamId": "steam-1", "futureField": true}})
|
||||
if err != nil || stored.Value["futureField"] != true {
|
||||
t.Fatalf("put plugin data=%+v err=%v", stored, err)
|
||||
}
|
||||
items, err := svc.ListPluginDataForSession(sessionID, domain.PluginDataFilter{PluginID: "server.scum", ServerInstanceID: "server-1", Collection: "scum_users"})
|
||||
items, err := svc.ListPluginDataForSession(sessionID, domain.PluginDataFilter{PluginID: "server.scum", ServerInstanceID: "server-1", Collection: "plugin_users"})
|
||||
if err != nil || len(items) != 1 || items[0].Key != "steam-1" || items[0].Value["steamId"] != "steam-1" {
|
||||
t.Fatalf("list plugin data=%+v err=%v", items, err)
|
||||
}
|
||||
if _, err := svc.ListPluginDataForSession(sessionID, domain.PluginDataFilter{PluginID: "other.plugin", ServerInstanceID: "server-1", Collection: "scum_users"}); err != ErrForbidden {
|
||||
if _, err := svc.ListPluginDataForSession(sessionID, domain.PluginDataFilter{PluginID: "other.plugin", ServerInstanceID: "server-1", Collection: "plugin_users"}); err != ErrForbidden {
|
||||
t.Fatalf("expected plugin isolation error, got %v", err)
|
||||
}
|
||||
otherOwner, err := svc.CreateUser(domain.User{ID: "other-plugin-data-owner", DisplayName: "Other plugin data owner", Email: "other-plugin-data@example.test", Status: domain.UserStatusActive, Roles: []string{"server-owner"}, PasswordHash: "secret-password"})
|
||||
@@ -32,13 +32,13 @@ func TestPluginDataIsOpaqueAndScopedToItsServerPlugin(t *testing.T) {
|
||||
if _, err := svc.CreateServerInstance(domain.ServerInstance{ID: "server-2", PluginID: plugin.ID, RunEndpointID: endpoint.ID, OwnerUserID: otherOwner.ID, Name: "Other server"}); err != nil {
|
||||
t.Fatalf("create other server: %v", err)
|
||||
}
|
||||
if _, err := svc.ListPluginDataForSession(sessionID, domain.PluginDataFilter{PluginID: plugin.ID, ServerInstanceID: "server-2", Collection: "scum_users"}); err != ErrForbidden {
|
||||
if _, err := svc.ListPluginDataForSession(sessionID, domain.PluginDataFilter{PluginID: plugin.ID, ServerInstanceID: "server-2", Collection: "plugin_users"}); err != ErrForbidden {
|
||||
t.Fatalf("expected server isolation error, got %v", err)
|
||||
}
|
||||
if _, err := svc.PutPluginDataForSession(sessionID, domain.PluginDataRecord{PluginID: plugin.ID, ServerInstanceID: "server-1", Collection: "settings", Key: "steam-1", Value: map[string]any{"enabled": true}}); err != nil {
|
||||
t.Fatalf("put second collection: %v", err)
|
||||
}
|
||||
items, err = svc.ListPluginDataForSession(sessionID, domain.PluginDataFilter{PluginID: plugin.ID, ServerInstanceID: "server-1", Collection: "scum_users"})
|
||||
items, err = svc.ListPluginDataForSession(sessionID, domain.PluginDataFilter{PluginID: plugin.ID, ServerInstanceID: "server-1", Collection: "plugin_users"})
|
||||
if err != nil || len(items) != 1 || items[0].Value["futureField"] != true {
|
||||
t.Fatalf("collection isolation values=%+v err=%v", items, err)
|
||||
}
|
||||
@@ -100,11 +100,11 @@ func TestPluginDataTransactionValidationFailureDoesNotPartiallyApply(t *testing.
|
||||
|
||||
func TestDeclaredSQLiteQueryDoesNotMutatePluginOrPlatformUserData(t *testing.T) {
|
||||
svc, plugin, endpoint, session, instance := createSQLiteQueryBridgeFixture(t)
|
||||
if _, err := svc.PutPluginDataForSession(session, domain.PluginDataRecord{PluginID: plugin.ID, ServerInstanceID: instance.ID, Collection: "scum_users", Key: "steam-keep", Value: map[string]any{"steamId": "steam-keep", "format": "plugin-local"}}); err != nil {
|
||||
if _, err := svc.PutPluginDataForSession(session, domain.PluginDataRecord{PluginID: plugin.ID, ServerInstanceID: instance.ID, Collection: "plugin_users", Key: "steam-keep", Value: map[string]any{"steamId": "steam-keep", "format": "plugin-local"}}); err != nil {
|
||||
t.Fatalf("seed plugin data: %v", err)
|
||||
}
|
||||
queued, err := svc.ExecutePluginBridgeAction(session, domain.PluginBridgeExecuteRequest{RequestID: "query-no-projection-1", PluginID: plugin.ID, RouteKey: "remote", ServerInstanceID: instance.ID, Action: domain.PluginBridgeActionRemoteAccessRequest, Payload: map[string]string{
|
||||
"capability": domain.JobCapabilityRemoteRunDBSQLiteQuery, "declarationKey": "scum-db-read", "targetKey": "scum-db.player-lookup", "idempotencyKey": "query-no-projection-1", "input.templateKey": "players.by-id",
|
||||
"capability": domain.JobCapabilityRemoteRunDBSQLiteQuery, "declarationKey": "game-db-read", "targetKey": "game-db.player-lookup", "idempotencyKey": "query-no-projection-1", "input.templateKey": "players.by-id",
|
||||
}})
|
||||
if err != nil || queued.Status != "queued" {
|
||||
t.Fatalf("queue declared query=%+v err=%v", queued, err)
|
||||
@@ -124,7 +124,7 @@ func TestDeclaredSQLiteQueryDoesNotMutatePluginOrPlatformUserData(t *testing.T)
|
||||
if err != nil {
|
||||
t.Fatalf("complete query job: %v", err)
|
||||
}
|
||||
items, err := svc.ListPluginDataForSession(session, domain.PluginDataFilter{PluginID: plugin.ID, ServerInstanceID: instance.ID, Collection: "scum_users"})
|
||||
items, err := svc.ListPluginDataForSession(session, domain.PluginDataFilter{PluginID: plugin.ID, ServerInstanceID: instance.ID, Collection: "plugin_users"})
|
||||
if err != nil || len(items) != 1 || items[0].Key != "steam-keep" || items[0].Value["format"] != "plugin-local" {
|
||||
t.Fatalf("declared query mutated plugin data=%+v err=%v", items, err)
|
||||
}
|
||||
@@ -162,7 +162,7 @@ func TestRunPollSchedulesAndProjectsDeclaredSQLiteQuery(t *testing.T) {
|
||||
svc, plugin, endpoint, session, instance := createSQLiteQueryBridgeFixture(t)
|
||||
plugin.GameClientBridge.QueryTemplates[0].PollIntervalSeconds = 3
|
||||
plugin.GameClientBridge.QueryTemplates[0].Projections = []domain.GameClientBridgeQueryProjectionDeclaration{{
|
||||
Collection: "scum_users", RowPath: "rows", MatchField: "kind", MatchValue: "player", UpsertKeys: []string{"steamId"},
|
||||
Collection: "plugin_users", RowPath: "rows", MatchField: "kind", MatchValue: "player", UpsertKeys: []string{"steamId"},
|
||||
FieldMappings: map[string]string{"steamId": "steamId", "displayName": "displayName"}, FixedValues: map[string]string{"source": "sqlite"}, ObservedAtField: "sampledAt",
|
||||
}, {
|
||||
Collection: "scum_trade_goods", RowPath: "rows", MatchField: "kind", MatchValue: "vehicle", UpsertKeys: []string{"code"},
|
||||
@@ -192,7 +192,7 @@ func TestRunPollSchedulesAndProjectsDeclaredSQLiteQuery(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("complete projection query job: %v", err)
|
||||
}
|
||||
items, err := svc.ListPluginDataForSession(session, domain.PluginDataFilter{PluginID: plugin.ID, ServerInstanceID: instance.ID, Collection: "scum_users"})
|
||||
items, err := svc.ListPluginDataForSession(session, domain.PluginDataFilter{PluginID: plugin.ID, ServerInstanceID: instance.ID, Collection: "plugin_users"})
|
||||
if err != nil || len(items) != 1 || items[0].Key != "steam-1" || items[0].Value["displayName"] != "Ada" || items[0].Value["source"] != "sqlite" || items[0].Value["sampledAt"] == nil {
|
||||
t.Fatalf("declared projection did not write scoped plugin data=%+v err=%v", items, err)
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ func TestRemoteAdapterRequestPropagatesTypedInputsToRunJob(t *testing.T) {
|
||||
plugin.DeclaredPermissions = append(plugin.DeclaredPermissions, "server.remote.access")
|
||||
plugin.RequiredRunCapabilities = append(plugin.RequiredRunCapabilities, capability)
|
||||
plugin.RemoteAccess = domain.GamePluginRemoteAccess{Methods: []string{"run"}, RunCapabilities: []string{capability}, DatabaseEngines: []string{"sqlite"}}
|
||||
plugin.RuntimeProfiles.TransportProfiles = append(plugin.RuntimeProfiles.TransportProfiles, domain.RuntimeTransportProfile{Key: "player-lookup", Kind: "sqlite", TargetKey: "scum-db.player-lookup", Capabilities: []string{capability}})
|
||||
plugin.RuntimeProfiles.TransportProfiles = append(plugin.RuntimeProfiles.TransportProfiles, domain.RuntimeTransportProfile{Key: "player-lookup", Kind: "sqlite", TargetKey: "game-db.player-lookup", Capabilities: []string{capability}})
|
||||
if err := svc.store.GamePlugins().Update(plugin); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -30,7 +30,7 @@ func TestRemoteAdapterRequestPropagatesTypedInputsToRunJob(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
inputs := map[string]string{"playerId": "steam-123", "limit": "25"}
|
||||
result, err := svc.RequestRemoteAdapterForSession(session, domain.RemoteAdapterRequest{ServerInstanceID: instance.ID, DeclarationKey: "player-lookup", TargetKey: "scum-db.player-lookup", Capability: capability, IdempotencyKey: "lookup-1", InputRef: "input://scum-db/player-lookup/lookup-1", Inputs: inputs})
|
||||
result, err := svc.RequestRemoteAdapterForSession(session, domain.RemoteAdapterRequest{ServerInstanceID: instance.ID, DeclarationKey: "player-lookup", TargetKey: "game-db.player-lookup", Capability: capability, IdempotencyKey: "lookup-1", InputRef: "input://game-db/player-lookup/lookup-1", Inputs: inputs})
|
||||
if err != nil {
|
||||
t.Fatalf("request remote adapter: %v", err)
|
||||
}
|
||||
@@ -39,7 +39,7 @@ func TestRemoteAdapterRequestPropagatesTypedInputsToRunJob(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if job.InputRef != "input://scum-db/player-lookup/lookup-1" || job.ExecutionInput.Inputs["playerId"] != "steam-123" || job.ExecutionInput.Inputs["limit"] != "25" {
|
||||
if job.InputRef != "input://game-db/player-lookup/lookup-1" || job.ExecutionInput.Inputs["playerId"] != "steam-123" || job.ExecutionInput.Inputs["limit"] != "25" {
|
||||
t.Fatalf("typed inputs were not propagated: %#v", job)
|
||||
}
|
||||
helloRequest := validRunControlHello()
|
||||
|
||||
@@ -181,6 +181,12 @@ type Core interface {
|
||||
ListGameClientBridgeCommandsForSession(string, domain.GameClientBridgeCommandFilter) ([]domain.GameClientBridgeCommand, error)
|
||||
GetGameClientBridgeCommandForSession(string, string) (domain.GameClientBridgeCommand, error)
|
||||
QueryGameClientBridgeSnapshotsForSession(string, domain.GameClientBridgeSnapshotQuery) ([]domain.GameClientBridgeSnapshot, error)
|
||||
ListSCUMUsersForSession(string, domain.SCUMUserFilter) ([]domain.SCUMUser, error)
|
||||
ListSCUMUserTrajectoriesForSession(string, domain.SCUMUserTrajectoryFilter) ([]domain.SCUMUserTrajectory, error)
|
||||
ListSCUMVehiclesForSession(string, domain.SCUMVehicleFilter) ([]domain.SCUMVehicle, error)
|
||||
ListSCUMVehicleTrajectoriesForSession(string, domain.SCUMVehicleTrajectoryFilter) ([]domain.SCUMVehicleTrajectory, error)
|
||||
ListSCUMVehicleLocksForSession(string, domain.SCUMVehicleLockFilter) ([]domain.SCUMVehicleLock, error)
|
||||
IngestSCUMFacts(domain.SCUMFactIngest) (domain.SCUMFactIngestResult, error)
|
||||
ListPluginDataForSession(string, domain.PluginDataFilter) ([]domain.PluginDataRecord, error)
|
||||
PutPluginDataForSession(string, domain.PluginDataRecord) (domain.PluginDataRecord, error)
|
||||
DeletePluginDataForSession(string, string, string, string, string) error
|
||||
|
||||
@@ -1766,8 +1766,8 @@ func TestCoreServiceDispatchesDeclaredSQLiteQueryTemplate(t *testing.T) {
|
||||
Action: domain.PluginBridgeActionRemoteAccessRequest,
|
||||
Payload: map[string]string{
|
||||
"capability": domain.JobCapabilityRemoteRunDBSQLiteQuery,
|
||||
"declarationKey": "scum-db-read",
|
||||
"targetKey": "scum-db.player-lookup",
|
||||
"declarationKey": "game-db-read",
|
||||
"targetKey": "game-db.player-lookup",
|
||||
"idempotencyKey": "query-template-dispatch-1",
|
||||
"input.templateKey": "players.by-id",
|
||||
"input.playerId": "steam-123",
|
||||
@@ -1799,9 +1799,9 @@ func TestCoreServiceDeniesUndeclaredOrMismatchedSQLiteQueryTemplateBeforeJob(t *
|
||||
declarationKey string
|
||||
targetKey string
|
||||
}{
|
||||
{name: "undeclared template", templateKey: "players.unknown", declarationKey: "scum-db-read", targetKey: "scum-db.player-lookup"},
|
||||
{name: "mismatched transport", templateKey: "players.by-id", declarationKey: "other-transport", targetKey: "scum-db.player-lookup"},
|
||||
{name: "mismatched target", templateKey: "players.by-id", declarationKey: "scum-db-read", targetKey: "scum-db.other"},
|
||||
{name: "undeclared template", templateKey: "players.unknown", declarationKey: "game-db-read", targetKey: "game-db.player-lookup"},
|
||||
{name: "mismatched transport", templateKey: "players.by-id", declarationKey: "other-transport", targetKey: "game-db.player-lookup"},
|
||||
{name: "mismatched target", templateKey: "players.by-id", declarationKey: "game-db-read", targetKey: "game-db.other"},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
@@ -1874,11 +1874,11 @@ func TestCoreServiceDispatchesSQLiteExecuteSQLText(t *testing.T) {
|
||||
Action: domain.PluginBridgeActionRemoteAccessRequest,
|
||||
Payload: map[string]string{
|
||||
"capability": domain.JobCapabilityRemoteRunDBSQLiteExecute,
|
||||
"declarationKey": "scum-db-read",
|
||||
"targetKey": "scum-db.player-lookup",
|
||||
"declarationKey": "game-db-read",
|
||||
"targetKey": "game-db.player-lookup",
|
||||
"idempotencyKey": "sqlite-execute-1",
|
||||
"input.mode": "execute",
|
||||
"input.sqlText": "UPDATE prisoner SET stamina = 855 WHERE id = 'steam-123';",
|
||||
"input.sqlText": "UPDATE players SET score = 855 WHERE id = 'player-123';",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
@@ -2155,9 +2155,9 @@ func createSQLiteQueryBridgeFixture(t *testing.T) (*CoreService, domain.GamePlug
|
||||
DatabaseEngines: []string{"sqlite"},
|
||||
}
|
||||
plugin.RuntimeProfiles.TransportProfiles = append(plugin.RuntimeProfiles.TransportProfiles, domain.RuntimeTransportProfile{
|
||||
Key: "scum-db-read",
|
||||
Key: "game-db-read",
|
||||
Kind: "sqlite",
|
||||
TargetKey: "scum-db.player-lookup",
|
||||
TargetKey: "game-db.player-lookup",
|
||||
Capabilities: []string{capability, executeCapability},
|
||||
})
|
||||
plugin.GameClientBridge = domain.GameClientBridgeManifest{
|
||||
@@ -2167,8 +2167,8 @@ func createSQLiteQueryBridgeFixture(t *testing.T) (*CoreService, domain.GamePlug
|
||||
Title: "Player lookup",
|
||||
Permission: "server.remote.access",
|
||||
Engine: "sqlite",
|
||||
TransportKey: "scum-db-read",
|
||||
TargetKey: "scum-db.player-lookup",
|
||||
TransportKey: "game-db-read",
|
||||
TargetKey: "game-db.player-lookup",
|
||||
ParameterSchemaRef: "schemas/queries/players.by-id.parameters.schema.json",
|
||||
ResultSchemaRef: "schemas/queries/players.by-id.result.schema.json",
|
||||
SQLRef: "sql/players.by-id.sql",
|
||||
|
||||
@@ -0,0 +1,410 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
"browser.local/platform/repo"
|
||||
"browser.local/platform/validator"
|
||||
)
|
||||
|
||||
func (svc *CoreService) ListSCUMUsersForSession(sessionID string, filter domain.SCUMUserFilter) ([]domain.SCUMUser, error) {
|
||||
if strings.TrimSpace(filter.ServerInstanceID) == "" {
|
||||
return nil, validationError("serverInstanceId is required")
|
||||
}
|
||||
if _, err := svc.GetServerInstanceForSession(sessionID, filter.ServerInstanceID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
users, err := svc.store.SCUMUsers().List(filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sort.SliceStable(users, func(i, j int) bool {
|
||||
if !users[i].LastActivityAt.Equal(users[j].LastActivityAt) {
|
||||
return users[i].LastActivityAt.After(users[j].LastActivityAt)
|
||||
}
|
||||
return users[i].SteamID < users[j].SteamID
|
||||
})
|
||||
limit := boundedSCUMLimit(filter.Limit, domain.SCUMDefaultListLimit)
|
||||
if len(users) > limit {
|
||||
users = users[:limit]
|
||||
}
|
||||
stamp := svc.now()
|
||||
for index := range users {
|
||||
if !users[index].LastLoginAt.IsZero() {
|
||||
users[index].LastLoginAge = stamp.Sub(users[index].LastLoginAt)
|
||||
}
|
||||
}
|
||||
return domain.CopySCUMUsers(users), nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) ListSCUMUserTrajectoriesForSession(sessionID string, filter domain.SCUMUserTrajectoryFilter) ([]domain.SCUMUserTrajectory, error) {
|
||||
if strings.TrimSpace(filter.ServerInstanceID) == "" {
|
||||
return nil, validationError("serverInstanceId is required")
|
||||
}
|
||||
if _, err := svc.GetServerInstanceForSession(sessionID, filter.ServerInstanceID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items, err := svc.store.SCUMUserTrajectories().List(filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sort.SliceStable(items, func(i, j int) bool {
|
||||
if !items[i].ObservedAt.Equal(items[j].ObservedAt) {
|
||||
return items[i].ObservedAt.After(items[j].ObservedAt)
|
||||
}
|
||||
return items[i].ID < items[j].ID
|
||||
})
|
||||
limit := boundedSCUMLimit(filter.Limit, domain.SCUMDefaultTrajectoryLimit)
|
||||
if len(items) > limit {
|
||||
items = items[:limit]
|
||||
}
|
||||
return domain.CopySCUMUserTrajectories(items), nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) ListSCUMVehiclesForSession(sessionID string, filter domain.SCUMVehicleFilter) ([]domain.SCUMVehicle, error) {
|
||||
if strings.TrimSpace(filter.ServerInstanceID) == "" {
|
||||
return nil, validationError("serverInstanceId is required")
|
||||
}
|
||||
if _, err := svc.GetServerInstanceForSession(sessionID, filter.ServerInstanceID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
vehicles, err := svc.store.SCUMVehicles().List(filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sort.SliceStable(vehicles, func(i, j int) bool {
|
||||
if !vehicles[i].LastObservedAt.Equal(vehicles[j].LastObservedAt) {
|
||||
return vehicles[i].LastObservedAt.After(vehicles[j].LastObservedAt)
|
||||
}
|
||||
return vehicles[i].GameVehicleID < vehicles[j].GameVehicleID
|
||||
})
|
||||
limit := boundedSCUMLimit(filter.Limit, domain.SCUMDefaultListLimit)
|
||||
if len(vehicles) > limit {
|
||||
vehicles = vehicles[:limit]
|
||||
}
|
||||
return domain.CopySCUMVehicles(vehicles), nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) ListSCUMVehicleTrajectoriesForSession(sessionID string, filter domain.SCUMVehicleTrajectoryFilter) ([]domain.SCUMVehicleTrajectory, error) {
|
||||
if strings.TrimSpace(filter.ServerInstanceID) == "" {
|
||||
return nil, validationError("serverInstanceId is required")
|
||||
}
|
||||
if _, err := svc.GetServerInstanceForSession(sessionID, filter.ServerInstanceID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items, err := svc.store.SCUMVehicleTrajectories().List(filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sort.SliceStable(items, func(i, j int) bool {
|
||||
if !items[i].ObservedAt.Equal(items[j].ObservedAt) {
|
||||
return items[i].ObservedAt.After(items[j].ObservedAt)
|
||||
}
|
||||
return items[i].ID < items[j].ID
|
||||
})
|
||||
limit := boundedSCUMLimit(filter.Limit, domain.SCUMDefaultTrajectoryLimit)
|
||||
if len(items) > limit {
|
||||
items = items[:limit]
|
||||
}
|
||||
return domain.CopySCUMVehicleTrajectories(items), nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) ListSCUMVehicleLocksForSession(sessionID string, filter domain.SCUMVehicleLockFilter) ([]domain.SCUMVehicleLock, error) {
|
||||
if strings.TrimSpace(filter.ServerInstanceID) == "" {
|
||||
return nil, validationError("serverInstanceId is required")
|
||||
}
|
||||
if _, err := svc.GetServerInstanceForSession(sessionID, filter.ServerInstanceID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items, err := svc.store.SCUMVehicleLocks().List(filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sort.SliceStable(items, func(i, j int) bool {
|
||||
if !items[i].LockedAt.Equal(items[j].LockedAt) {
|
||||
return items[i].LockedAt.After(items[j].LockedAt)
|
||||
}
|
||||
return items[i].ID < items[j].ID
|
||||
})
|
||||
limit := boundedSCUMLimit(filter.Limit, domain.SCUMDefaultTrajectoryLimit)
|
||||
if len(items) > limit {
|
||||
items = items[:limit]
|
||||
}
|
||||
return domain.CopySCUMVehicleLocks(items), nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) IngestSCUMFacts(batch domain.SCUMFactIngest) (domain.SCUMFactIngestResult, error) {
|
||||
batch = domain.CopySCUMFactIngest(batch)
|
||||
if strings.TrimSpace(batch.ServerInstanceID) == "" {
|
||||
return domain.SCUMFactIngestResult{}, validationError("serverInstanceId is required")
|
||||
}
|
||||
if len(batch.Users) == 0 && len(batch.Vehicles) == 0 {
|
||||
return domain.SCUMFactIngestResult{}, validationError("SCUM facts are required")
|
||||
}
|
||||
if len(batch.Users) > 1000 || len(batch.Vehicles) > 1000 {
|
||||
return domain.SCUMFactIngestResult{}, validationError("SCUM fact batch is too large")
|
||||
}
|
||||
if err := svc.validateRunSession(batch.RunEndpointID, batch.SessionToken); err != nil {
|
||||
return domain.SCUMFactIngestResult{}, err
|
||||
}
|
||||
instance, err := svc.store.ServerInstances().Get(batch.ServerInstanceID)
|
||||
if err != nil {
|
||||
return domain.SCUMFactIngestResult{}, err
|
||||
}
|
||||
if instance.RunEndpointID != batch.RunEndpointID {
|
||||
return domain.SCUMFactIngestResult{}, ErrForbidden
|
||||
}
|
||||
stamp := svc.now()
|
||||
welcomeCount := 0
|
||||
for _, fact := range batch.Users {
|
||||
if err := svc.ingestSCUMUserFact(instance, fact, stamp, &welcomeCount); err != nil {
|
||||
return domain.SCUMFactIngestResult{}, err
|
||||
}
|
||||
}
|
||||
for _, fact := range batch.Vehicles {
|
||||
if err := svc.ingestSCUMVehicleFact(instance, fact, stamp); err != nil {
|
||||
return domain.SCUMFactIngestResult{}, err
|
||||
}
|
||||
}
|
||||
return domain.SCUMFactIngestResult{Accepted: true, AcceptedUserCount: len(batch.Users), AcceptedVehicleCount: len(batch.Vehicles), WelcomeQueuedCount: welcomeCount, ServerTime: stamp}, nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) ingestSCUMUserFact(instance domain.ServerInstance, fact domain.SCUMUserFact, stamp time.Time, welcomeCount *int) error {
|
||||
steamID := strings.TrimSpace(fact.SteamID)
|
||||
if steamID == "" {
|
||||
return validationError("SCUM user steamId is required")
|
||||
}
|
||||
observedAt := nonZeroTime(fact.ObservedAt, stamp)
|
||||
loginAt := nonZeroTime(fact.LoginObservedAt, observedAt)
|
||||
userID := scumUserID(instance.ID, steamID)
|
||||
user, err := svc.store.SCUMUsers().Get(userID)
|
||||
created := false
|
||||
if errors.Is(err, repo.ErrNotFound) {
|
||||
created = true
|
||||
user = domain.SCUMUser{ID: userID, ServerInstanceID: instance.ID, SteamID: steamID, RegisteredAt: loginAt, CreatedAt: stamp}
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
previousActivity := user.LastActivityAt
|
||||
freshLogin := created || (fact.Login && (previousActivity.IsZero() || loginAt.Sub(previousActivity) > domain.SCUMUserOfflineAfter))
|
||||
moved := fact.Online && fact.Position != nil && userPositionMoved(user, fact.Position)
|
||||
if fact.Login || created {
|
||||
user.LastLoginAt = loginAt
|
||||
}
|
||||
user.DisplayName = firstNonBlank(fact.DisplayName, user.DisplayName)
|
||||
if fact.SquadID != nil {
|
||||
user.SquadID = *fact.SquadID
|
||||
}
|
||||
user.LastLoginIP = firstNonBlank(fact.LoginIP, user.LastLoginIP)
|
||||
user.Online = fact.Online
|
||||
user.LastActivityAt = latestTime(user.LastActivityAt, observedAt)
|
||||
if fact.Position != nil {
|
||||
user.X, user.Y, user.Z = floatPtr(fact.Position.X), floatPtr(fact.Position.Y), floatPtr(fact.Position.Z)
|
||||
}
|
||||
if fact.BankBalance != nil {
|
||||
user.BankBalance = int64Ptr(*fact.BankBalance)
|
||||
}
|
||||
if fact.GoldBars != nil {
|
||||
user.GoldBars = int64Ptr(*fact.GoldBars)
|
||||
}
|
||||
user.GameVehicleID = strings.TrimSpace(fact.RiddenGameVehicleID)
|
||||
if user.GameVehicleID != "" {
|
||||
user.RiddenVehicleID = scumVehicleID(instance.ID, user.GameVehicleID)
|
||||
} else {
|
||||
user.RiddenVehicleID = ""
|
||||
}
|
||||
user.UpdatedAt = stamp
|
||||
if created {
|
||||
if err := svc.store.SCUMUsers().Create(user); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err := svc.store.SCUMUsers().Update(user); err != nil {
|
||||
return err
|
||||
}
|
||||
if moved {
|
||||
track := domain.SCUMUserTrajectory{ID: scumTrajectoryID("scum-user-track", instance.ID, steamID, observedAt), ServerInstanceID: instance.ID, SCUMUserID: user.ID, SteamID: steamID, DisplayName: user.DisplayName, X: fact.Position.X, Y: fact.Position.Y, Z: fact.Position.Z, RiddenVehicleID: user.RiddenVehicleID, GameVehicleID: user.GameVehicleID, ObservedAt: observedAt, CreatedAt: stamp}
|
||||
if err := createUnlessDuplicate(svc.store.SCUMUserTrajectories(), track); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if freshLogin {
|
||||
queued, queueErr := svc.queueSCUMWelcomeMessage(instance, user, stamp)
|
||||
if queueErr != nil {
|
||||
user.WelcomeLastError = queueErr.Error()
|
||||
} else if queued {
|
||||
user.WelcomeQueuedAt = stamp.Add(domain.SCUMWelcomeMessageDelay)
|
||||
*welcomeCount = *welcomeCount + 1
|
||||
}
|
||||
user.UpdatedAt = stamp
|
||||
if err := svc.store.SCUMUsers().Update(user); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) ingestSCUMVehicleFact(instance domain.ServerInstance, fact domain.SCUMVehicleFact, stamp time.Time) error {
|
||||
gameVehicleID := strings.TrimSpace(fact.GameVehicleID)
|
||||
if gameVehicleID == "" {
|
||||
return validationError("SCUM vehicle gameVehicleId is required")
|
||||
}
|
||||
observedAt := nonZeroTime(fact.ObservedAt, stamp)
|
||||
vehicleID := scumVehicleID(instance.ID, gameVehicleID)
|
||||
vehicle, err := svc.store.SCUMVehicles().Get(vehicleID)
|
||||
created := false
|
||||
if errors.Is(err, repo.ErrNotFound) {
|
||||
created = true
|
||||
vehicle = domain.SCUMVehicle{ID: vehicleID, ServerInstanceID: instance.ID, GameVehicleID: gameVehicleID, Exists: true, CreatedAt: stamp}
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
moved := fact.Position != nil && vehiclePositionMoved(vehicle, fact.Position)
|
||||
vehicle.VehicleClass = firstNonBlank(fact.VehicleClass, vehicle.VehicleClass)
|
||||
vehicle.DisplayName = firstNonBlank(fact.DisplayName, vehicle.DisplayName)
|
||||
if fact.Exists != nil {
|
||||
vehicle.Exists = *fact.Exists
|
||||
}
|
||||
if fact.Locked != nil {
|
||||
vehicle.Locked = *fact.Locked
|
||||
}
|
||||
if fact.Position != nil {
|
||||
vehicle.X, vehicle.Y, vehicle.Z = floatPtr(fact.Position.X), floatPtr(fact.Position.Y), floatPtr(fact.Position.Z)
|
||||
}
|
||||
vehicle.LastObservedAt = latestTime(vehicle.LastObservedAt, observedAt)
|
||||
vehicle.UpdatedAt = stamp
|
||||
if created {
|
||||
if err := svc.store.SCUMVehicles().Create(vehicle); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err := svc.store.SCUMVehicles().Update(vehicle); err != nil {
|
||||
return err
|
||||
}
|
||||
if moved {
|
||||
track := domain.SCUMVehicleTrajectory{ID: scumTrajectoryID("scum-vehicle-track", instance.ID, gameVehicleID, observedAt), ServerInstanceID: instance.ID, SCUMVehicleID: vehicle.ID, GameVehicleID: gameVehicleID, VehicleClass: vehicle.VehicleClass, X: fact.Position.X, Y: fact.Position.Y, Z: fact.Position.Z, ObservedAt: observedAt, CreatedAt: stamp}
|
||||
if err := createUnlessDuplicate(svc.store.SCUMVehicleTrajectories(), track); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if strings.TrimSpace(fact.LockedBySteamID) != "" && !fact.LockedAt.IsZero() {
|
||||
lockUserID := scumUserID(instance.ID, strings.TrimSpace(fact.LockedBySteamID))
|
||||
lock := domain.SCUMVehicleLock{ID: scumTrajectoryID("scum-vehicle-lock", instance.ID, gameVehicleID+":"+fact.LockedBySteamID, fact.LockedAt), ServerInstanceID: instance.ID, SCUMVehicleID: vehicle.ID, GameVehicleID: gameVehicleID, SCUMUserID: lockUserID, SteamID: strings.TrimSpace(fact.LockedBySteamID), LockedAt: fact.LockedAt, CreatedAt: stamp}
|
||||
if err := createUnlessDuplicate(svc.store.SCUMVehicleLocks(), lock); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) queueSCUMWelcomeMessage(instance domain.ServerInstance, user domain.SCUMUser, stamp time.Time) (bool, error) {
|
||||
if instance.State != domain.ServerInstanceStateRunning {
|
||||
return false, nil
|
||||
}
|
||||
resolution, err := svc.resolveSourceRCONDispatch(instance)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
message := fmt.Sprintf("欢迎 %s 回到服务器", firstNonBlank(user.DisplayName, user.SteamID))
|
||||
request := domain.SourceRCONCommandRequest{ServerInstanceID: instance.ID, Kind: domain.SourceRCONCommandKindChat, ChatType: 4, Message: message, IdempotencyKey: scumWelcomeIdempotencyKey(instance.ID, user.SteamID, user.LastLoginAt)}
|
||||
if err := validator.ValidateSourceRCONCommandRequest(request); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if existing, err := svc.store.Jobs().GetByIdempotency(instance.RunEndpointID, request.IdempotencyKey); err == nil {
|
||||
return existing.QueueEligibleAt.Equal(stamp.Add(domain.SCUMWelcomeMessageDelay)) || !existing.QueueEligibleAt.IsZero(), nil
|
||||
} else if !errors.Is(err, repo.ErrNotFound) {
|
||||
return false, err
|
||||
}
|
||||
command := sourceRCONCommandText(request)
|
||||
jobID := jobIDFromParts("job-source-rcon", instance.ID, request.IdempotencyKey)
|
||||
if err := svc.sourceRCONCommands.Put(jobID, command); err != nil {
|
||||
return false, err
|
||||
}
|
||||
job := domain.Job{ID: jobID, ServerInstanceID: instance.ID, RunEndpointID: instance.RunEndpointID, Capability: domain.JobCapabilityRemoteRunRCONCommand, TargetKey: resolution.transport.TargetKey, InputRef: "input://source-rcon/" + jobID, IdempotencyKey: request.IdempotencyKey, State: domain.JobStateQueued, Progress: domain.JobProgress{Percent: 0, Message: "SCUM welcome message queued"}, RetryPolicy: domain.JobRetryPolicy{MaxAttempts: 1, InitialBackoffSeconds: 1, MaxBackoffSeconds: 1}, QueueEligibleAt: stamp.Add(domain.SCUMWelcomeMessageDelay), ExecutionInput: domain.JobExecutionInput{WorkspaceScope: resolution.binding.ProfileKey, RemoteAdapterKey: resolution.transport.Key, RemoteAdapterKind: string(domain.RemoteAdapterRCON), TimeoutSeconds: sourceRCONTimeoutSeconds, PluginID: resolution.plugin.ID, SourceRCON: resolution.plan}}
|
||||
created, err := svc.CreateJob(job)
|
||||
if err != nil {
|
||||
svc.sourceRCONCommands.Delete(jobID)
|
||||
return false, err
|
||||
}
|
||||
return created.ID == jobID, nil
|
||||
}
|
||||
|
||||
func userPositionMoved(user domain.SCUMUser, position *domain.SCUMPosition) bool {
|
||||
if position == nil || user.X == nil || user.Y == nil || user.Z == nil {
|
||||
return true
|
||||
}
|
||||
return *user.X != position.X || *user.Y != position.Y || *user.Z != position.Z
|
||||
}
|
||||
|
||||
func vehiclePositionMoved(vehicle domain.SCUMVehicle, position *domain.SCUMPosition) bool {
|
||||
if position == nil || vehicle.X == nil || vehicle.Y == nil || vehicle.Z == nil {
|
||||
return true
|
||||
}
|
||||
return *vehicle.X != position.X || *vehicle.Y != position.Y || *vehicle.Z != position.Z
|
||||
}
|
||||
|
||||
func boundedSCUMLimit(requested int, fallback int) int {
|
||||
if requested <= 0 {
|
||||
return fallback
|
||||
}
|
||||
if requested > 1000 {
|
||||
return 1000
|
||||
}
|
||||
return requested
|
||||
}
|
||||
|
||||
func scumUserID(serverID string, steamID string) string {
|
||||
return "scum-user-" + fingerprintID(serverID, strings.TrimSpace(steamID))
|
||||
}
|
||||
|
||||
func scumVehicleID(serverID string, gameVehicleID string) string {
|
||||
return "scum-vehicle-" + fingerprintID(serverID, strings.TrimSpace(gameVehicleID))
|
||||
}
|
||||
|
||||
func scumTrajectoryID(prefix string, serverID string, subjectID string, observedAt time.Time) string {
|
||||
return prefix + "-" + fingerprintID(serverID, strings.TrimSpace(subjectID)+"\x00"+observedAt.UTC().Format(time.RFC3339Nano))
|
||||
}
|
||||
|
||||
func scumWelcomeIdempotencyKey(serverID string, steamID string, loginAt time.Time) string {
|
||||
return "scum-welcome:" + fingerprintID(serverID, strings.TrimSpace(steamID)+"\x00"+loginAt.UTC().Format(time.RFC3339Nano))
|
||||
}
|
||||
|
||||
func nonZeroTime(value time.Time, fallback time.Time) time.Time {
|
||||
if value.IsZero() {
|
||||
return fallback
|
||||
}
|
||||
return value.UTC()
|
||||
}
|
||||
|
||||
func latestTime(left time.Time, right time.Time) time.Time {
|
||||
if left.IsZero() || right.After(left) {
|
||||
return right
|
||||
}
|
||||
return left
|
||||
}
|
||||
|
||||
func firstNonBlank(values ...string) string {
|
||||
for _, value := range values {
|
||||
if strings.TrimSpace(value) != "" {
|
||||
return strings.TrimSpace(value)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func floatPtr(value float64) *float64 { return &value }
|
||||
func int64Ptr(value int64) *int64 { return &value }
|
||||
|
||||
type scumCreateOnlyRepository[T any] interface {
|
||||
Create(T) error
|
||||
}
|
||||
|
||||
func createUnlessDuplicate[T any](repository scumCreateOnlyRepository[T], value T) error {
|
||||
if err := repository.Create(value); err != nil && !errors.Is(err, repo.ErrDuplicate) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
)
|
||||
|
||||
func TestSCUMFactIngestMaintainsPlatformTablesAndWelcomeQueue(t *testing.T) {
|
||||
svc, session, runSession, instance := newSourceRCONFixture(t)
|
||||
squadID := int64(42)
|
||||
bank := int64(1200)
|
||||
gold := int64(3)
|
||||
exists := true
|
||||
locked := true
|
||||
observedAt := fixedTime.Add(-1 * time.Minute)
|
||||
|
||||
result, err := svc.IngestSCUMFacts(domain.SCUMFactIngest{
|
||||
RunEndpointID: instance.RunEndpointID,
|
||||
SessionToken: runSession,
|
||||
ServerInstanceID: instance.ID,
|
||||
Users: []domain.SCUMUserFact{{
|
||||
SteamID: "76561198000000001", DisplayName: "Mira", SquadID: &squadID, LoginIP: "203.0.113.7", Online: true, Login: true,
|
||||
ObservedAt: observedAt, LoginObservedAt: observedAt, Position: &domain.SCUMPosition{X: 10, Y: 20, Z: 3}, BankBalance: &bank, GoldBars: &gold, RiddenGameVehicleID: "game-veh-1",
|
||||
}},
|
||||
Vehicles: []domain.SCUMVehicleFact{{
|
||||
GameVehicleID: "game-veh-1", VehicleClass: "BPC_Laika_C", DisplayName: "Laika", Exists: &exists, Locked: &locked, LockedBySteamID: "76561198000000001", LockedAt: observedAt, ObservedAt: observedAt, Position: &domain.SCUMPosition{X: 400, Y: 200, Z: 0},
|
||||
}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ingest SCUM facts: %v", err)
|
||||
}
|
||||
if !result.Accepted || result.AcceptedUserCount != 1 || result.AcceptedVehicleCount != 1 || result.WelcomeQueuedCount != 1 {
|
||||
t.Fatalf("unexpected ingest result: %+v", result)
|
||||
}
|
||||
|
||||
users, err := svc.ListSCUMUsersForSession(session, domain.SCUMUserFilter{ServerInstanceID: instance.ID})
|
||||
if err != nil || len(users) != 1 {
|
||||
t.Fatalf("list SCUM users: users=%+v err=%v", users, err)
|
||||
}
|
||||
user := users[0]
|
||||
if user.SteamID != "76561198000000001" || user.SquadID != squadID || user.LastLoginIP != "203.0.113.7" || !user.Online || user.BankBalance == nil || *user.BankBalance != bank || user.GoldBars == nil || *user.GoldBars != gold || user.RiddenVehicleID == "" || user.GameVehicleID != "game-veh-1" {
|
||||
t.Fatalf("unexpected SCUM user: %+v", user)
|
||||
}
|
||||
if user.LastLoginAge != time.Minute {
|
||||
t.Fatalf("expected one-minute login age, got %s", user.LastLoginAge)
|
||||
}
|
||||
|
||||
jobs, err := svc.store.Jobs().List(domain.JobFilter{ServerInstanceID: instance.ID})
|
||||
if err != nil || len(jobs) != 1 {
|
||||
t.Fatalf("expected one queued welcome job, jobs=%+v err=%v", jobs, err)
|
||||
}
|
||||
if jobs[0].Capability != domain.JobCapabilityRemoteRunRCONCommand || !jobs[0].QueueEligibleAt.Equal(fixedTime.Add(domain.SCUMWelcomeMessageDelay)) {
|
||||
t.Fatalf("unexpected welcome job: %+v", jobs[0])
|
||||
}
|
||||
|
||||
userTracks, err := svc.ListSCUMUserTrajectoriesForSession(session, domain.SCUMUserTrajectoryFilter{ServerInstanceID: instance.ID})
|
||||
if err != nil || len(userTracks) != 1 || userTracks[0].RiddenVehicleID != user.RiddenVehicleID {
|
||||
t.Fatalf("unexpected user trajectory rows: rows=%+v err=%v", userTracks, err)
|
||||
}
|
||||
vehicles, err := svc.ListSCUMVehiclesForSession(session, domain.SCUMVehicleFilter{ServerInstanceID: instance.ID})
|
||||
if err != nil || len(vehicles) != 1 || vehicles[0].GameVehicleID != "game-veh-1" || !vehicles[0].Exists || !vehicles[0].Locked {
|
||||
t.Fatalf("unexpected SCUM vehicles: vehicles=%+v err=%v", vehicles, err)
|
||||
}
|
||||
vehicleTracks, err := svc.ListSCUMVehicleTrajectoriesForSession(session, domain.SCUMVehicleTrajectoryFilter{ServerInstanceID: instance.ID})
|
||||
if err != nil || len(vehicleTracks) != 1 || vehicleTracks[0].SCUMVehicleID != vehicles[0].ID {
|
||||
t.Fatalf("unexpected vehicle trajectory rows: rows=%+v err=%v", vehicleTracks, err)
|
||||
}
|
||||
locks, err := svc.ListSCUMVehicleLocksForSession(session, domain.SCUMVehicleLockFilter{ServerInstanceID: instance.ID})
|
||||
if err != nil || len(locks) != 1 || locks[0].SCUMUserID != user.ID || locks[0].SCUMVehicleID != vehicles[0].ID {
|
||||
t.Fatalf("unexpected vehicle lock history: locks=%+v err=%v", locks, err)
|
||||
}
|
||||
|
||||
_, err = svc.IngestSCUMFacts(domain.SCUMFactIngest{
|
||||
RunEndpointID: instance.RunEndpointID, SessionToken: runSession, ServerInstanceID: instance.ID,
|
||||
Users: []domain.SCUMUserFact{{SteamID: "76561198000000001", Online: true, ObservedAt: observedAt, Position: &domain.SCUMPosition{X: 10, Y: 20, Z: 3}}},
|
||||
Vehicles: []domain.SCUMVehicleFact{{GameVehicleID: "game-veh-1", ObservedAt: observedAt, Position: &domain.SCUMPosition{X: 400, Y: 200, Z: 0}}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("re-ingest unchanged SCUM facts: %v", err)
|
||||
}
|
||||
userTracks, _ = svc.ListSCUMUserTrajectoriesForSession(session, domain.SCUMUserTrajectoryFilter{ServerInstanceID: instance.ID})
|
||||
vehicleTracks, _ = svc.ListSCUMVehicleTrajectoriesForSession(session, domain.SCUMVehicleTrajectoryFilter{ServerInstanceID: instance.ID})
|
||||
if len(userTracks) != 1 || len(vehicleTracks) != 1 {
|
||||
t.Fatalf("unchanged positions must not create trajectory rows, users=%+v vehicles=%+v", userTracks, vehicleTracks)
|
||||
}
|
||||
|
||||
_, err = svc.IngestSCUMFacts(domain.SCUMFactIngest{
|
||||
RunEndpointID: instance.RunEndpointID, SessionToken: runSession, ServerInstanceID: instance.ID,
|
||||
Users: []domain.SCUMUserFact{{SteamID: "76561198000000001", Online: false, ObservedAt: fixedTime, Position: &domain.SCUMPosition{X: 11, Y: 21, Z: 4}}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ingest offline SCUM user fact: %v", err)
|
||||
}
|
||||
userTracks, _ = svc.ListSCUMUserTrajectoriesForSession(session, domain.SCUMUserTrajectoryFilter{ServerInstanceID: instance.ID})
|
||||
if len(userTracks) != 1 {
|
||||
t.Fatalf("offline users must not create trajectory rows, rows=%+v", userTracks)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSCUMFactIngestQueuesFreshWelcomeAfterActivityGap(t *testing.T) {
|
||||
svc, session, runSession, instance := newSourceRCONFixture(t)
|
||||
firstLogin := fixedTime.Add(-20 * time.Minute)
|
||||
secondLogin := fixedTime.Add(-4 * time.Minute)
|
||||
|
||||
for _, loginAt := range []time.Time{firstLogin, secondLogin} {
|
||||
result, err := svc.IngestSCUMFacts(domain.SCUMFactIngest{RunEndpointID: instance.RunEndpointID, SessionToken: runSession, ServerInstanceID: instance.ID, Users: []domain.SCUMUserFact{{SteamID: "76561198000000002", DisplayName: "Noah", Online: true, Login: true, ObservedAt: loginAt, LoginObservedAt: loginAt}}})
|
||||
if err != nil {
|
||||
t.Fatalf("ingest login at %s: %v", loginAt, err)
|
||||
}
|
||||
if result.WelcomeQueuedCount != 1 {
|
||||
t.Fatalf("expected fresh welcome for login at %s, got %+v", loginAt, result)
|
||||
}
|
||||
}
|
||||
|
||||
users, err := svc.ListSCUMUsersForSession(session, domain.SCUMUserFilter{ServerInstanceID: instance.ID, SteamID: "76561198000000002"})
|
||||
if err != nil || len(users) != 1 {
|
||||
t.Fatalf("list gap user: users=%+v err=%v", users, err)
|
||||
}
|
||||
if users[0].LastLoginAge != 4*time.Minute {
|
||||
t.Fatalf("expected last login age to show the second login, got %s", users[0].LastLoginAge)
|
||||
}
|
||||
jobs, err := svc.store.Jobs().List(domain.JobFilter{ServerInstanceID: instance.ID})
|
||||
if err != nil || len(jobs) != 2 {
|
||||
t.Fatalf("expected two welcome jobs after activity gap, jobs=%+v err=%v", jobs, err)
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ func TestObservabilityValidatorsBoundMetricsBackupsAndRemoteTargets(t *testing.T
|
||||
if err := ValidateRemoteAdapterRequest(domain.RemoteAdapterRequest{ServerInstanceID: "server-1", DeclarationKey: "ftp", TargetKey: "tcp://host", Capability: "remote.ftp.read", IdempotencyKey: "request-1"}); err == nil {
|
||||
t.Fatal("expected unsafe remote target rejection")
|
||||
}
|
||||
if err := ValidateRemoteAdapterRequest(domain.RemoteAdapterRequest{ServerInstanceID: "server-1", DeclarationKey: "sqlite-db", TargetKey: "scum-db", Capability: domain.JobCapabilityRemoteRunDBSQLiteExecute, IdempotencyKey: "sql-execute-1", Inputs: map[string]string{"mode": "execute", "sqlText": "UPDATE prisoner SET stamina = 855 WHERE id = 'steam-123';"}}); err != nil {
|
||||
if err := ValidateRemoteAdapterRequest(domain.RemoteAdapterRequest{ServerInstanceID: "server-1", DeclarationKey: "sqlite-db", TargetKey: "game-db", Capability: domain.JobCapabilityRemoteRunDBSQLiteExecute, IdempotencyKey: "sql-execute-1", Inputs: map[string]string{"mode": "execute", "sqlText": "UPDATE players SET score = 855 WHERE id = 'player-123';"}}); err != nil {
|
||||
t.Fatalf("expected SQL text input to validate: %v", err)
|
||||
}
|
||||
if err := ValidateRemoteAdapterRequest(domain.RemoteAdapterRequest{ServerInstanceID: "server-1", DeclarationKey: "rcon", TargetKey: "scum-rcon", Capability: domain.JobCapabilityRemoteRunRCONCommand, IdempotencyKey: "rcon-command-1", Inputs: map[string]string{"command": "#Login password=opaque /Users/operator note tcp://127.0.0.1:7777"}}); err != nil {
|
||||
|
||||
@@ -8,14 +8,14 @@ import (
|
||||
)
|
||||
|
||||
func TestValidateGamePluginRuntimeProfilesAllowsSafeCasePreservingDataTargetPath(t *testing.T) {
|
||||
profiles := validRuntimeDataTargetProfiles("SCUM/Saved/SaveFiles/SCUM.db")
|
||||
profiles := validRuntimeDataTargetProfiles("Game/Saved/SaveFiles/state.db")
|
||||
if err := ValidateGamePluginRuntimeProfiles(profiles); err != nil {
|
||||
t.Fatalf("expected safe cross-platform data target path to validate: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateGamePluginRuntimeProfilesRejectsUnsafeDataTargetPath(t *testing.T) {
|
||||
for _, sourcePath := range []string{"../SCUM.db", "/SCUM.db", `SCUM\\Saved\\SCUM.db`, "C:/SCUM.db", "https://example.test/SCUM.db"} {
|
||||
for _, sourcePath := range []string{"../state.db", "/state.db", `Game\\Saved\\state.db`, "C:/state.db", "https://example.test/state.db"} {
|
||||
t.Run(sourcePath, func(t *testing.T) {
|
||||
err := ValidateGamePluginRuntimeProfiles(validRuntimeDataTargetProfiles(sourcePath))
|
||||
if err == nil || !strings.Contains(err.Error(), "sourcePath must be a safe relative path") {
|
||||
@@ -28,18 +28,18 @@ func TestValidateGamePluginRuntimeProfilesRejectsUnsafeDataTargetPath(t *testing
|
||||
func validRuntimeDataTargetProfiles(sourcePath string) domain.GamePluginRuntimeProfiles {
|
||||
return domain.GamePluginRuntimeProfiles{
|
||||
TransportProfiles: []domain.RuntimeTransportProfile{{
|
||||
Key: "scum-database",
|
||||
Key: "game-database",
|
||||
Kind: "sqlite",
|
||||
TargetKey: "scum-database",
|
||||
TargetKey: "game-database",
|
||||
Capabilities: []string{domain.JobCapabilityRemoteRunDBSQLiteQuery},
|
||||
}},
|
||||
DataTargets: []domain.RuntimeDataTarget{{
|
||||
Key: "scum-database",
|
||||
Key: "game-database",
|
||||
Kind: "sqlite.snapshot",
|
||||
TransportKey: "scum-database",
|
||||
TransportKey: "game-database",
|
||||
SourceRootKey: "server-root",
|
||||
SourcePath: sourcePath,
|
||||
WorkspaceKey: "databases/scum-database",
|
||||
WorkspaceKey: "databases/game-database",
|
||||
RefreshPolicy: "on-demand-snapshot",
|
||||
MaxBytes: 1024 * 1024,
|
||||
Platforms: []string{"windows"},
|
||||
|
||||
Reference in New Issue
Block a user