Rebuild SCUM plugin data ownership
This commit is contained in:
@@ -23,7 +23,7 @@ func (svc *CoreService) SaveGameGiftCatalogForSession(sessionID, serverID string
|
||||
if err = svc.authorizeServerLifecycle(sessionID, serverID); err != nil {
|
||||
return domain.GameGiftCatalog{}, err
|
||||
}
|
||||
if err = svc.validateGiftItems(serverID, request.GameVersion, request.Items); err != nil {
|
||||
if err = validateGiftItems(request.GameVersion, request.Items); err != nil {
|
||||
return domain.GameGiftCatalog{}, err
|
||||
}
|
||||
stamp := svc.now()
|
||||
@@ -60,7 +60,7 @@ func (svc *CoreService) PublishGameGiftCatalogForSession(sessionID, catalogID st
|
||||
if err = svc.authorizeServerLifecycle(sessionID, catalog.ServerInstanceID); err != nil {
|
||||
return domain.GameGiftRevision{}, err
|
||||
}
|
||||
if err = svc.validateGiftItems(catalog.ServerInstanceID, catalog.GameVersion, catalog.DraftItems); err != nil {
|
||||
if err = validateGiftItems(catalog.GameVersion, catalog.DraftItems); err != nil {
|
||||
return domain.GameGiftRevision{}, err
|
||||
}
|
||||
revisions, err := svc.store.GameGiftRevisions().List(domain.GameGiftRevisionFilter{CatalogID: catalog.ID})
|
||||
@@ -111,7 +111,7 @@ func (svc *CoreService) RequestGameGiftGrantForSession(sessionID, serverID strin
|
||||
if err != nil || revision.ServerInstanceID != serverID {
|
||||
return domain.GameGiftGrant{}, repo.ErrNotFound
|
||||
}
|
||||
if err = svc.validateGiftItems(serverID, revision.GameVersion, revision.Items); err != nil {
|
||||
if err = validateGiftItems(revision.GameVersion, revision.Items); err != nil {
|
||||
return domain.GameGiftGrant{}, err
|
||||
}
|
||||
player, err := svc.store.GamePlayers().Get(request.GamePlayerRecordID)
|
||||
@@ -148,7 +148,7 @@ func (svc *CoreService) ApproveGameGiftGrantForSession(sessionID, grantID string
|
||||
if grant.Status != domain.GameGiftGrantPendingApproval {
|
||||
return domain.GameGiftGrant{}, validationError("gift grant is not awaiting approval")
|
||||
}
|
||||
if err = svc.validateGiftItems(grant.ServerInstanceID, grant.GameVersion, grant.Items); err != nil {
|
||||
if err = validateGiftItems(grant.GameVersion, grant.Items); err != nil {
|
||||
return domain.GameGiftGrant{}, err
|
||||
}
|
||||
sessions, err := svc.store.GamePlayerSessions().List(domain.GamePlayerSessionFilter{GamePlayerRecordID: grant.GamePlayerRecordID, OpenOnly: true})
|
||||
@@ -198,55 +198,23 @@ func (svc *CoreService) ListGameGiftGrantsForSession(sessionID, serverID string)
|
||||
sort.Slice(grants, func(i, j int) bool { return grants[i].CreatedAt.After(grants[j].CreatedAt) })
|
||||
return grants, nil
|
||||
}
|
||||
func (svc *CoreService) validateGiftItems(serverID, version string, items []domain.GameGiftItem) error {
|
||||
instance, err := svc.store.ServerInstances().Get(serverID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
plugin, err := svc.store.GamePlugins().Get(instance.PluginID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
catalog, ok := scumGiftCatalogFromPlugin(plugin, version)
|
||||
if !ok {
|
||||
return validationError("installed SCUM plugin has no verified gift item catalog for this game version")
|
||||
func validateGiftItems(version string, items []domain.GameGiftItem) error {
|
||||
if _, ok := domain.SCUMGiftCatalogForVersion(version); !ok {
|
||||
return validationError("SCUM game version has no verified gift item catalog")
|
||||
}
|
||||
if len(items) == 0 || len(items) > 8 {
|
||||
return validationError("gift requires 1 to 8 catalog items")
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for index, item := range items {
|
||||
def, ok := scumGiftItem(catalog, item.CatalogItemKey)
|
||||
if !ok || seen[item.CatalogItemKey] || item.Quantity < 1 || item.Quantity > def.MaximumQuantity {
|
||||
for _, item := range items {
|
||||
def, ok := domain.SCUMGiftItemForVersion(version, item.CatalogItemKey)
|
||||
if !ok || seen[item.CatalogItemKey] || item.Quantity < 1 || item.Quantity > def.MaximumQuantity || item.Label != def.Label {
|
||||
return validationError("gift item is not valid for this SCUM version")
|
||||
}
|
||||
items[index].Label = def.Label
|
||||
seen[item.CatalogItemKey] = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func scumGiftCatalogFromPlugin(plugin domain.GamePlugin, version string) (domain.SCUMGiftItemCatalog, bool) {
|
||||
for _, asset := range plugin.LifecycleAssets {
|
||||
if !strings.HasPrefix(asset.Path, "data-packs/") || !strings.HasSuffix(asset.Path, "/gift-items.json") || strings.TrimSpace(asset.Content) == "" {
|
||||
continue
|
||||
}
|
||||
catalog, ok := domain.ParseSCUMGiftItemCatalog(asset.Content)
|
||||
if ok && catalog.GameVersion == version {
|
||||
return catalog, true
|
||||
}
|
||||
}
|
||||
return domain.SCUMGiftItemCatalog{}, false
|
||||
}
|
||||
|
||||
func scumGiftItem(catalog domain.SCUMGiftItemCatalog, key string) (domain.SCUMGiftItemDefinition, bool) {
|
||||
for _, item := range catalog.Items {
|
||||
if item.Key == key {
|
||||
return item, true
|
||||
}
|
||||
}
|
||||
return domain.SCUMGiftItemDefinition{}, false
|
||||
}
|
||||
func giftDeliveryPayload(grant domain.GameGiftGrant) map[string]any {
|
||||
items := make([]any, len(grant.Items))
|
||||
for i, item := range grant.Items {
|
||||
|
||||
@@ -100,7 +100,6 @@ func gameGiftFixture(t *testing.T, online bool) (*CoreService, string, domain.Ga
|
||||
svc, clock := newGameClientBridgeService(t)
|
||||
plugin, _ := svc.store.GamePlugins().Get("game.scum")
|
||||
plugin.GameClientBridge.Commands = []domain.GameClientBridgeCommandDeclaration{{Type: domain.SCUMRewardDeliverCommandType, ApprovalLevel: domain.GameClientBridgeApprovalLevelOperator, TimeoutSeconds: 120, MaxPayloadBytes: 4096}, {Type: domain.SCUMGiftNotificationCommandType, ApprovalLevel: domain.GameClientBridgeApprovalLevelOperator, TimeoutSeconds: 60, MaxPayloadBytes: 2048}}
|
||||
plugin.LifecycleAssets = append(plugin.LifecycleAssets, domain.PluginAssetFile{Path: "data-packs/scum-db-v57/gift-items.json", Content: `{"gameVersion":"0.9.700.90357","items":[{"key":"bandage","label":"绷带","maximumQuantity":20},{"key":"water-bottle","label":"饮用水","maximumQuantity":10},{"key":"improvised-spear","label":"简易长矛","maximumQuantity":2}]}`})
|
||||
if err := svc.store.GamePlugins().Update(plugin); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
"browser.local/platform/repo"
|
||||
)
|
||||
|
||||
func (svc *CoreService) ListPluginDataForSession(sessionID string, filter domain.PluginDataFilter) ([]domain.PluginDataRecord, error) {
|
||||
if err := svc.authorizePluginData(sessionID, filter.PluginID, filter.ServerInstanceID, filter.Collection); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
values, err := svc.store.PluginDataRecords().List(filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if filter.Limit > 0 && len(values) > filter.Limit {
|
||||
values = values[:filter.Limit]
|
||||
}
|
||||
return values, nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) PutPluginDataForSession(sessionID string, value domain.PluginDataRecord) (domain.PluginDataRecord, error) {
|
||||
if err := svc.authorizePluginData(sessionID, value.PluginID, value.ServerInstanceID, value.Collection); err != nil {
|
||||
return domain.PluginDataRecord{}, err
|
||||
}
|
||||
if strings.TrimSpace(value.Key) == "" {
|
||||
return domain.PluginDataRecord{}, validationError("plugin data key is required")
|
||||
}
|
||||
if value.Value == nil {
|
||||
return domain.PluginDataRecord{}, validationError("plugin data value is required")
|
||||
}
|
||||
value.ID = pluginDataID(value.ServerInstanceID, value.PluginID, value.Collection, value.Key)
|
||||
stamp := svc.now()
|
||||
existing, err := svc.store.PluginDataRecords().Get(value.ID)
|
||||
if err == repo.ErrNotFound {
|
||||
value.CreatedAt, value.UpdatedAt = stamp, stamp
|
||||
if err := svc.store.PluginDataRecords().Create(value); err != nil {
|
||||
return domain.PluginDataRecord{}, err
|
||||
}
|
||||
return domain.CopyPluginDataRecord(value), nil
|
||||
}
|
||||
if err != nil {
|
||||
return domain.PluginDataRecord{}, err
|
||||
}
|
||||
existing.Value, existing.UpdatedAt = domain.CopyGameClientBridgePayload(value.Value), stamp
|
||||
if err := svc.store.PluginDataRecords().Update(existing); err != nil {
|
||||
return domain.PluginDataRecord{}, err
|
||||
}
|
||||
return domain.CopyPluginDataRecord(existing), nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) authorizePluginData(sessionID, pluginID, serverInstanceID, collection string) error {
|
||||
if strings.TrimSpace(pluginID) == "" || strings.TrimSpace(serverInstanceID) == "" || strings.TrimSpace(collection) == "" {
|
||||
return validationError("pluginId, serverInstanceId, and collection are required")
|
||||
}
|
||||
if err := svc.authorizeServerLifecycle(sessionID, serverInstanceID); err != nil {
|
||||
return err
|
||||
}
|
||||
instance, err := svc.store.ServerInstances().Get(serverInstanceID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if instance.PluginID != pluginID {
|
||||
return ErrForbidden
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func pluginDataID(serverID, pluginID, collection, key string) string {
|
||||
return "plugin-data-" + fingerprintID(serverID, pluginID+"\x00"+collection+"\x00"+key)
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
)
|
||||
|
||||
func TestPluginDataIsOpaqueAndScopedToItsServerPlugin(t *testing.T) {
|
||||
svc := newTestCoreService()
|
||||
plugin, endpoint := createPluginAndRunEndpoint(t, svc)
|
||||
owner, err := svc.RegisterUser(domain.UserRegistration{DisplayName: "Plugin data owner", Email: "plugin-data@example.test", Password: "secret-password"})
|
||||
if err != nil {
|
||||
t.Fatalf("register owner: %v", err)
|
||||
}
|
||||
if _, err := svc.CreateServerInstance(domain.ServerInstance{ID: "server-1", PluginID: plugin.ID, RunEndpointID: endpoint.ID, OwnerUserID: owner.User.ID, Name: "SCUM"}); err != nil {
|
||||
t.Fatalf("create server: %v", err)
|
||||
}
|
||||
sessionID := owner.SessionID
|
||||
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}})
|
||||
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"})
|
||||
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 {
|
||||
t.Fatalf("expected plugin isolation error, got %v", err)
|
||||
}
|
||||
}
|
||||
@@ -191,6 +191,8 @@ type Core interface {
|
||||
ListGameClientBridgeCommandsForSession(string, domain.GameClientBridgeCommandFilter) ([]domain.GameClientBridgeCommand, error)
|
||||
GetGameClientBridgeCommandForSession(string, string) (domain.GameClientBridgeCommand, error)
|
||||
QueryGameClientBridgeSnapshotsForSession(string, domain.GameClientBridgeSnapshotQuery) ([]domain.GameClientBridgeSnapshot, error)
|
||||
ListPluginDataForSession(string, domain.PluginDataFilter) ([]domain.PluginDataRecord, error)
|
||||
PutPluginDataForSession(string, domain.PluginDataRecord) (domain.PluginDataRecord, error)
|
||||
PushRunUpdateForSession(string, domain.RunUpdateRequest) (domain.RunUpdateJob, error)
|
||||
ListRunUpdateJobsForSession(string, string) ([]domain.RunUpdateJob, error)
|
||||
GetDependencyCatalogForSession(string, string) (domain.DependencyCatalog, error)
|
||||
@@ -231,7 +233,6 @@ type Core interface {
|
||||
ListSCUMVehiclesForSession(string, domain.SCUMProjectionFilter) ([]domain.SCUMVehicle, error)
|
||||
ListSCUMFlagsForSession(string, domain.SCUMProjectionFilter) ([]domain.SCUMFlag, error)
|
||||
ListSCUMCurrentPositionsForSession(string, domain.SCUMProjectionFilter) ([]domain.SCUMCurrentPosition, error)
|
||||
ListSCUMDataRowsForSession(string, domain.SCUMProjectionFilter) ([]domain.SCUMDataRow, error)
|
||||
RequestSCUMOperationForSession(string, string, domain.SCUMOperationRequest) (domain.SCUMOperationRequest, error)
|
||||
ListSCUMOperationsForSession(string, domain.SCUMOperationRequestFilter) ([]domain.SCUMOperationRequest, error)
|
||||
ApproveSCUMOperationForSession(string, string) (domain.SCUMOperationRequest, error)
|
||||
|
||||
@@ -59,28 +59,12 @@ func (svc *CoreService) ApplySCUMObservationResult(result domain.SCUMObservation
|
||||
return observation, nil
|
||||
}
|
||||
freshness := domain.SCUMProjectionFreshnessState{Status: domain.SCUMProjectionFresh, ObservationID: observation.ID, Source: observation.Source, QueryKey: observation.QueryKey, Sequence: observation.Sequence, Checksum: observation.Checksum, ObservedAt: observation.ObservedAt, ReceivedAt: observation.ReceivedAt}
|
||||
target, err := svc.scumRowTarget(result.PluginID, result.QueryKey)
|
||||
if err != nil {
|
||||
return domain.SCUMDataObservation{}, err
|
||||
}
|
||||
if err := svc.applySCUMRows(target, result.PluginID, result.QueryKey, result.ServerInstanceID, result.Rows, freshness); err != nil {
|
||||
if err := svc.applySCUMRows(result.QueryKey, result.ServerInstanceID, result.Rows, freshness); err != nil {
|
||||
return domain.SCUMDataObservation{}, err
|
||||
}
|
||||
return observation, nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) ListSCUMDataRowsForSession(sessionID string, filter domain.SCUMProjectionFilter) ([]domain.SCUMDataRow, error) {
|
||||
if err := svc.authorizeServerLifecycle(sessionID, filter.ServerInstanceID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
values, err := svc.store.SCUMDataRows().List(filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
limitSCUMProjectionSlice(&values, filter.Limit)
|
||||
return values, nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) ListSCUMPlayerLiveStatesForSession(sessionID string, filter domain.SCUMProjectionFilter) ([]domain.SCUMPlayerLiveState, error) {
|
||||
if err := svc.authorizeServerLifecycle(sessionID, filter.ServerInstanceID); err != nil {
|
||||
return nil, err
|
||||
@@ -190,39 +174,44 @@ func (svc *CoreService) upsertSCUMObservation(observation domain.SCUMDataObserva
|
||||
return svc.store.SCUMDataObservations().Create(observation)
|
||||
}
|
||||
|
||||
func (svc *CoreService) applySCUMRows(target domain.SCUMRowTargetDeclaration, pluginID, queryKey, serverID string, rows []map[string]any, freshness domain.SCUMProjectionFreshnessState) error {
|
||||
if target.TargetTable != "" {
|
||||
func (svc *CoreService) applySCUMRows(queryKey, serverID string, rows []map[string]any, freshness domain.SCUMProjectionFreshnessState) error {
|
||||
lower := strings.ToLower(queryKey)
|
||||
if strings.Contains(lower, "player") || strings.Contains(lower, "profile") || strings.Contains(lower, "economy") {
|
||||
for _, row := range rows {
|
||||
if err := svc.upsertSCUMDataRow(target, pluginID, queryKey, serverID, row, freshness); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
// Compatibility declarations retain the old projections without guessing from substrings.
|
||||
for _, row := range rows {
|
||||
switch queryKey {
|
||||
case "scum.player.profile":
|
||||
if err := svc.applySCUMPlayerRow(serverID, row, freshness); err != nil {
|
||||
return err
|
||||
}
|
||||
case "scum.squads":
|
||||
if err := svc.applySCUMSquadRow(serverID, row, freshness); err != nil {
|
||||
return err
|
||||
}
|
||||
case "scum.squad-members":
|
||||
}
|
||||
}
|
||||
if strings.Contains(lower, "squad-member") || strings.Contains(lower, "squad.member") || strings.Contains(lower, "member") {
|
||||
for _, row := range rows {
|
||||
if err := svc.applySCUMSquadMemberRow(serverID, row, freshness); err != nil {
|
||||
return err
|
||||
}
|
||||
case "scum.vehicles":
|
||||
}
|
||||
} else if strings.Contains(lower, "squad") {
|
||||
for _, row := range rows {
|
||||
if err := svc.applySCUMSquadRow(serverID, row, freshness); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if strings.Contains(lower, "vehicle") {
|
||||
for _, row := range rows {
|
||||
if err := svc.applySCUMVehicleRow(serverID, row, freshness); err != nil {
|
||||
return err
|
||||
}
|
||||
case "scum.flags":
|
||||
}
|
||||
}
|
||||
if strings.Contains(lower, "flag") {
|
||||
for _, row := range rows {
|
||||
if err := svc.applySCUMFlagRow(serverID, row, freshness); err != nil {
|
||||
return err
|
||||
}
|
||||
case "scum.positions":
|
||||
}
|
||||
}
|
||||
if strings.Contains(lower, "position") || strings.Contains(lower, "coordinate") {
|
||||
for _, row := range rows {
|
||||
if err := svc.applySCUMPositionRow(serverID, row, freshness); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -231,78 +220,6 @@ func (svc *CoreService) applySCUMRows(target domain.SCUMRowTargetDeclaration, pl
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc *CoreService) scumRowTarget(pluginID, queryKey string) (domain.SCUMRowTargetDeclaration, error) {
|
||||
plugin, err := svc.store.GamePlugins().Get(pluginID)
|
||||
if err != nil {
|
||||
return domain.SCUMRowTargetDeclaration{}, err
|
||||
}
|
||||
for _, template := range plugin.GameClientBridge.QueryTemplates {
|
||||
if template.Key == queryKey && template.RowTarget != nil {
|
||||
return domain.CopySCUMRowTargetDeclaration(*template.RowTarget), nil
|
||||
}
|
||||
}
|
||||
if _, ok := legacySCUMQueryKeys[queryKey]; ok {
|
||||
return domain.SCUMRowTargetDeclaration{}, nil
|
||||
}
|
||||
return domain.SCUMRowTargetDeclaration{}, validationError("queryKey does not declare a SCUM row target")
|
||||
}
|
||||
|
||||
var legacySCUMQueryKeys = map[string]struct{}{
|
||||
"scum.player.profile": {}, "scum.squads": {}, "scum.squad-members": {}, "scum.vehicles": {}, "scum.flags": {}, "scum.positions": {},
|
||||
}
|
||||
|
||||
func (svc *CoreService) upsertSCUMDataRow(target domain.SCUMRowTargetDeclaration, pluginID, queryKey, serverID string, row map[string]any, freshness domain.SCUMProjectionFreshnessState) error {
|
||||
table := domain.SCUMDataSet(strings.TrimSpace(target.TargetTable))
|
||||
if !validSCUMDataSet(table) || len(target.UpsertKeys) == 0 {
|
||||
return validationError("SCUM row target is invalid")
|
||||
}
|
||||
fields := map[string]any{}
|
||||
for destination, source := range target.ColumnMappings {
|
||||
if value, ok := row[source]; ok {
|
||||
fields[destination] = value
|
||||
}
|
||||
}
|
||||
keyValues := make([]string, 0, len(target.UpsertKeys))
|
||||
for _, key := range target.UpsertKeys {
|
||||
value, ok := fields[key]
|
||||
if !ok {
|
||||
value, ok = row[key]
|
||||
}
|
||||
text := firstString(map[string]any{"value": value}, "value")
|
||||
if !ok || text == "" {
|
||||
return validationError("SCUM row is missing declared upsert key " + key)
|
||||
}
|
||||
keyValues = append(keyValues, text)
|
||||
}
|
||||
upsertKey := strings.Join(keyValues, "\x00")
|
||||
id := scumProjectionID(string(table), serverID, upsertKey)
|
||||
value, err := svc.store.SCUMDataRows().Get(id)
|
||||
if err == repo.ErrNotFound {
|
||||
value = domain.SCUMDataRow{ID: id, ServerInstanceID: serverID, TargetTable: table, UpsertKey: upsertKey, CreatedAt: svc.now()}
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
if isProjectionOlder(freshness, value.Freshness) {
|
||||
return nil
|
||||
}
|
||||
value.Fields = domain.CopyGameClientBridgePayload(fields)
|
||||
value.Payload = domain.CopyGameClientBridgePayload(row)
|
||||
value.PluginID, value.QueryKey, value.Freshness, value.UpdatedAt = pluginID, queryKey, freshness, svc.now()
|
||||
if err == repo.ErrNotFound {
|
||||
return svc.store.SCUMDataRows().Create(value)
|
||||
}
|
||||
return svc.store.SCUMDataRows().Update(value)
|
||||
}
|
||||
|
||||
func validSCUMDataSet(value domain.SCUMDataSet) bool {
|
||||
switch value {
|
||||
case domain.SCUMDataSetUsers, domain.SCUMDataSetSquads, domain.SCUMDataSetMembers, domain.SCUMDataSetVehicles, domain.SCUMDataSetFlags, domain.SCUMDataSetActivity, domain.SCUMDataSetGiftEvents, domain.SCUMDataSetMapPoints:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (svc *CoreService) applySCUMPlayerRow(serverID string, row map[string]any, freshness domain.SCUMProjectionFreshnessState) error {
|
||||
gamePlayerID := firstString(row, "gamePlayerId", "playerId", "steamId", "steam_id")
|
||||
profileID := firstString(row, "userProfileId", "user_profile_id", "profileId")
|
||||
@@ -641,27 +558,8 @@ func (svc *CoreService) upsertSCUMPosition(position domain.SCUMCurrentPosition)
|
||||
|
||||
func (svc *CoreService) markSCUMQueryStale(result domain.SCUMObservationResult, reason string) error {
|
||||
freshness := domain.SCUMProjectionFreshnessState{Status: domain.SCUMProjectionStale, ObservationID: scumObservationID(result), Source: result.Source, QueryKey: result.QueryKey, Sequence: result.Sequence, Checksum: result.Checksum, StaleReason: reason, ObservedAt: result.ObservedAt, ReceivedAt: result.ReceivedAt}
|
||||
target, err := svc.scumRowTarget(result.PluginID, result.QueryKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if target.TargetTable != "" {
|
||||
values, err := svc.store.SCUMDataRows().List(domain.SCUMProjectionFilter{ServerInstanceID: result.ServerInstanceID, TargetTable: domain.SCUMDataSet(target.TargetTable)})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, value := range values {
|
||||
if !isProjectionOlder(freshness, value.Freshness) {
|
||||
value.Freshness, value.UpdatedAt = freshness, svc.now()
|
||||
if err := svc.store.SCUMDataRows().Update(value); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
switch result.QueryKey {
|
||||
case "scum.player.profile":
|
||||
lower := strings.ToLower(result.QueryKey)
|
||||
if strings.Contains(lower, "player") || strings.Contains(lower, "profile") || strings.Contains(lower, "economy") {
|
||||
values, err := svc.store.SCUMPlayerLiveStates().List(domain.SCUMProjectionFilter{ServerInstanceID: result.ServerInstanceID})
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -675,7 +573,8 @@ func (svc *CoreService) markSCUMQueryStale(result domain.SCUMObservationResult,
|
||||
}
|
||||
}
|
||||
}
|
||||
case "scum.squads", "scum.squad-members":
|
||||
}
|
||||
if strings.Contains(lower, "squad") {
|
||||
values, err := svc.store.SCUMSquads().List(domain.SCUMProjectionFilter{ServerInstanceID: result.ServerInstanceID})
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -689,7 +588,8 @@ func (svc *CoreService) markSCUMQueryStale(result domain.SCUMObservationResult,
|
||||
}
|
||||
}
|
||||
}
|
||||
case "scum.vehicles":
|
||||
}
|
||||
if strings.Contains(lower, "vehicle") {
|
||||
values, err := svc.store.SCUMVehicles().List(domain.SCUMProjectionFilter{ServerInstanceID: result.ServerInstanceID})
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -703,7 +603,8 @@ func (svc *CoreService) markSCUMQueryStale(result domain.SCUMObservationResult,
|
||||
}
|
||||
}
|
||||
}
|
||||
case "scum.flags":
|
||||
}
|
||||
if strings.Contains(lower, "flag") {
|
||||
values, err := svc.store.SCUMFlags().List(domain.SCUMProjectionFilter{ServerInstanceID: result.ServerInstanceID})
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -108,29 +108,3 @@ func TestSCUMLoginLogsProjectLiveStateAndDatabaseSaveTimeDoesNotProveOnline(t *t
|
||||
t.Fatalf("last_save_time was incorrectly treated as online proof: %+v", states[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestSCUMObservationUsesDeclaredRowTargetInsteadOfQueryKeyName(t *testing.T) {
|
||||
svc, _ := newRegisteredLogIngestService(t)
|
||||
plugin, err := svc.store.GamePlugins().Get("server.scum")
|
||||
if err != nil {
|
||||
t.Fatalf("get plugin: %v", err)
|
||||
}
|
||||
plugin.GameClientBridge.QueryTemplates = append(plugin.GameClientBridge.QueryTemplates, domain.GameClientBridgeQueryTemplateDeclaration{Key: "v57.catalog.people", RowTarget: &domain.SCUMRowTargetDeclaration{TargetTable: string(domain.SCUMDataSetUsers), UpsertKeys: []string{"profileId"}, ColumnMappings: map[string]string{"profileId": "user_profile_id", "name": "display_name"}}})
|
||||
if err := svc.store.GamePlugins().Update(plugin); err != nil {
|
||||
t.Fatalf("update plugin: %v", err)
|
||||
}
|
||||
_, err = svc.ApplySCUMObservationResult(domain.SCUMObservationResult{ServerInstanceID: "server-1", PluginID: "server.scum", Source: "run.sqlite.read", QueryKey: "v57.catalog.people", Sequence: 1, ObservedAt: time.Now().UTC(), Rows: []map[string]any{{"user_profile_id": "profile-1", "display_name": "Moon", "unmapped": "kept"}}})
|
||||
if err != nil {
|
||||
t.Fatalf("apply declared data row: %v", err)
|
||||
}
|
||||
rows, err := svc.store.SCUMDataRows().List(domain.SCUMProjectionFilter{ServerInstanceID: "server-1", TargetTable: domain.SCUMDataSetUsers})
|
||||
if err != nil || len(rows) != 1 {
|
||||
t.Fatalf("rows=%+v err=%v", rows, err)
|
||||
}
|
||||
if rows[0].Fields["profileId"] != "profile-1" || rows[0].Payload["unmapped"] != "kept" {
|
||||
t.Fatalf("unexpected declared row: %+v", rows[0])
|
||||
}
|
||||
if states, err := svc.store.SCUMPlayerLiveStates().List(domain.SCUMProjectionFilter{ServerInstanceID: "server-1"}); err != nil || len(states) != 0 {
|
||||
t.Fatalf("query key leaked into legacy projection dispatch: states=%+v err=%v", states, err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user