Align SCUM operations with reference tools
This commit is contained in:
@@ -34,7 +34,7 @@ func (h *coreHandlers) serverSCUMActivity(w http.ResponseWriter, r *http.Request
|
||||
}
|
||||
|
||||
func (h *coreHandlers) serverSCUMGifts(w http.ResponseWriter, r *http.Request) {
|
||||
h.serverSCUMDataSet(w, r, domain.SCUMDataSetGifts)
|
||||
h.serverSCUMDataSet(w, r, domain.SCUMDataSetGiftEvents)
|
||||
}
|
||||
|
||||
func (h *coreHandlers) serverSCUMMapPoints(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package domain
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const SCUMRewardDeliverCommandType = "reward.deliver"
|
||||
const SCUMGiftNotificationCommandType = "player.notify"
|
||||
@@ -15,7 +19,32 @@ type SCUMGiftItemCatalog struct {
|
||||
Items []SCUMGiftItemDefinition
|
||||
}
|
||||
|
||||
var SCUMGiftItemCatalogs = []SCUMGiftItemCatalog{{GameVersion: "0.9.700.90357", Items: []SCUMGiftItemDefinition{{Key: "bandage", Label: "绷带", MaximumQuantity: 20}, {Key: "water-bottle", Label: "饮用水", MaximumQuantity: 10}, {Key: "improvised-spear", Label: "简易长矛", MaximumQuantity: 2}}}}
|
||||
func ParseSCUMGiftItemCatalog(content string) (SCUMGiftItemCatalog, bool) {
|
||||
var catalog struct {
|
||||
GameVersion string `json:"gameVersion"`
|
||||
Items []struct {
|
||||
Key string `json:"key"`
|
||||
Label string `json:"label"`
|
||||
MaximumQuantity int `json:"maximumQuantity"`
|
||||
} `json:"items"`
|
||||
}
|
||||
if json.Unmarshal([]byte(content), &catalog) != nil || strings.TrimSpace(catalog.GameVersion) == "" || len(catalog.Items) == 0 {
|
||||
return SCUMGiftItemCatalog{}, false
|
||||
}
|
||||
result := SCUMGiftItemCatalog{GameVersion: catalog.GameVersion, Items: make([]SCUMGiftItemDefinition, 0, len(catalog.Items))}
|
||||
seen := map[string]struct{}{}
|
||||
for _, item := range catalog.Items {
|
||||
if strings.TrimSpace(item.Key) == "" || strings.TrimSpace(item.Label) == "" || item.MaximumQuantity < 1 {
|
||||
return SCUMGiftItemCatalog{}, false
|
||||
}
|
||||
if _, ok := seen[item.Key]; ok {
|
||||
return SCUMGiftItemCatalog{}, false
|
||||
}
|
||||
seen[item.Key] = struct{}{}
|
||||
result.Items = append(result.Items, SCUMGiftItemDefinition{Key: item.Key, Label: item.Label, MaximumQuantity: item.MaximumQuantity})
|
||||
}
|
||||
return result, true
|
||||
}
|
||||
|
||||
type GameGiftItem struct {
|
||||
CatalogItemKey string
|
||||
@@ -122,23 +151,3 @@ func CopyGameGiftGrant(value GameGiftGrant) GameGiftGrant {
|
||||
value.Items = CopyGameGiftItems(value.Items)
|
||||
return value
|
||||
}
|
||||
func SCUMGiftCatalogForVersion(version string) (SCUMGiftItemCatalog, bool) {
|
||||
for _, catalog := range SCUMGiftItemCatalogs {
|
||||
if catalog.GameVersion == version {
|
||||
return catalog, true
|
||||
}
|
||||
}
|
||||
return SCUMGiftItemCatalog{}, false
|
||||
}
|
||||
func SCUMGiftItemForVersion(version, key string) (SCUMGiftItemDefinition, bool) {
|
||||
catalog, ok := SCUMGiftCatalogForVersion(version)
|
||||
if !ok {
|
||||
return SCUMGiftItemDefinition{}, false
|
||||
}
|
||||
for _, item := range catalog.Items {
|
||||
if item.Key == key {
|
||||
return item, true
|
||||
}
|
||||
}
|
||||
return SCUMGiftItemDefinition{}, false
|
||||
}
|
||||
|
||||
@@ -88,14 +88,16 @@ type SCUMObservationResult struct {
|
||||
type SCUMDataSet string
|
||||
|
||||
const (
|
||||
SCUMDataSetUsers SCUMDataSet = "scum_users"
|
||||
SCUMDataSetSquads SCUMDataSet = "scum_squads"
|
||||
SCUMDataSetMembers SCUMDataSet = "scum_squad_members"
|
||||
SCUMDataSetVehicles SCUMDataSet = "scum_vehicles"
|
||||
SCUMDataSetFlags SCUMDataSet = "scum_flags"
|
||||
SCUMDataSetActivity SCUMDataSet = "scum_activity_events"
|
||||
SCUMDataSetGifts SCUMDataSet = "scum_gift_catalogs"
|
||||
SCUMDataSetMapPoints SCUMDataSet = "scum_map_points"
|
||||
SCUMDataSetUsers SCUMDataSet = "scum_users"
|
||||
SCUMDataSetSquads SCUMDataSet = "scum_squads"
|
||||
SCUMDataSetMembers SCUMDataSet = "scum_squad_members"
|
||||
SCUMDataSetVehicles SCUMDataSet = "scum_vehicles"
|
||||
SCUMDataSetFlags SCUMDataSet = "scum_flags"
|
||||
SCUMDataSetActivity SCUMDataSet = "scum_activity_events"
|
||||
// Gift events are facts observed in SCUM's finished_timed_gift_spawner table.
|
||||
// Platform-owned gift catalogs, revisions, and grants use their own repositories.
|
||||
SCUMDataSetGiftEvents SCUMDataSet = "scum_gift_events"
|
||||
SCUMDataSetMapPoints SCUMDataSet = "scum_map_points"
|
||||
)
|
||||
|
||||
type SCUMDataRow struct {
|
||||
|
||||
@@ -74,8 +74,7 @@ type GameGiftGrantListResponse struct {
|
||||
func (r GameGiftCatalogRequest) ToDomain() domain.GameGiftCatalogRequest {
|
||||
items := make([]domain.GameGiftItem, len(r.Items))
|
||||
for i, item := range r.Items {
|
||||
def, _ := domain.SCUMGiftItemForVersion(r.GameVersion, item.CatalogItemKey)
|
||||
items[i] = domain.GameGiftItem{CatalogItemKey: item.CatalogItemKey, Label: def.Label, Quantity: item.Quantity}
|
||||
items[i] = domain.GameGiftItem{CatalogItemKey: item.CatalogItemKey, Quantity: item.Quantity}
|
||||
}
|
||||
return domain.GameGiftCatalogRequest{ID: r.ID, Name: r.Name, GameVersion: r.GameVersion, Items: items}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ type GameGiftCatalog struct {
|
||||
UpdatedAt time.Time `json:"updatedAt" db:"updated_at"`
|
||||
}
|
||||
|
||||
func (GameGiftCatalog) TableName() string { return "game_gift_catalogs" }
|
||||
func (GameGiftCatalog) TableName() string { return "scum_gift_catalogs" }
|
||||
|
||||
// GameGiftRevision is an immutable gift item snapshot.
|
||||
type GameGiftRevision struct {
|
||||
@@ -27,7 +27,7 @@ type GameGiftRevision struct {
|
||||
PublishedAt time.Time `json:"publishedAt" db:"published_at"`
|
||||
}
|
||||
|
||||
func (GameGiftRevision) TableName() string { return "game_gift_revisions" }
|
||||
func (GameGiftRevision) TableName() string { return "scum_gift_revisions" }
|
||||
|
||||
// GameGiftGrant records a directed frozen gift lifecycle without raw game commands.
|
||||
type GameGiftGrant struct {
|
||||
@@ -42,4 +42,4 @@ type GameGiftGrant struct {
|
||||
UpdatedAt time.Time `json:"updatedAt" db:"updated_at"`
|
||||
}
|
||||
|
||||
func (GameGiftGrant) TableName() string { return "game_gift_grants" }
|
||||
func (GameGiftGrant) TableName() string { return "scum_gift_grants" }
|
||||
|
||||
@@ -253,7 +253,7 @@ CREATE TABLE IF NOT EXISTS platform_metadata_snapshots (
|
||||
if err != nil {
|
||||
return fmt.Errorf("create mysql metadata snapshot table: %w", err)
|
||||
}
|
||||
for _, table := range []string{"scum_sync_runs", "scum_users", "scum_squads", "scum_squad_members", "scum_vehicles", "scum_flags", "scum_activity_events", "scum_gift_catalogs", "scum_map_points"} {
|
||||
for _, table := range []string{"scum_sync_runs", "scum_users", "scum_squads", "scum_squad_members", "scum_vehicles", "scum_flags", "scum_activity_events", "scum_gift_events", "scum_map_points"} {
|
||||
statement := fmt.Sprintf(`CREATE TABLE IF NOT EXISTS %s (
|
||||
id VARCHAR(191) PRIMARY KEY,
|
||||
server_instance_id VARCHAR(191) NOT NULL,
|
||||
@@ -396,8 +396,8 @@ func mysqlSCUMTable(target domain.SCUMDataSet) (string, bool) {
|
||||
return "scum_flags", true
|
||||
case domain.SCUMDataSetActivity:
|
||||
return "scum_activity_events", true
|
||||
case domain.SCUMDataSetGifts:
|
||||
return "scum_gift_catalogs", true
|
||||
case domain.SCUMDataSetGiftEvents:
|
||||
return "scum_gift_events", true
|
||||
case domain.SCUMDataSetMapPoints:
|
||||
return "scum_map_points", true
|
||||
default:
|
||||
|
||||
@@ -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 = validateGiftItems(request.GameVersion, request.Items); err != nil {
|
||||
if err = svc.validateGiftItems(serverID, 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 = validateGiftItems(catalog.GameVersion, catalog.DraftItems); err != nil {
|
||||
if err = svc.validateGiftItems(catalog.ServerInstanceID, 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 = validateGiftItems(revision.GameVersion, revision.Items); err != nil {
|
||||
if err = svc.validateGiftItems(serverID, 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 = validateGiftItems(grant.GameVersion, grant.Items); err != nil {
|
||||
if err = svc.validateGiftItems(grant.ServerInstanceID, grant.GameVersion, grant.Items); err != nil {
|
||||
return domain.GameGiftGrant{}, err
|
||||
}
|
||||
sessions, err := svc.store.GamePlayerSessions().List(domain.GamePlayerSessionFilter{GamePlayerRecordID: grant.GamePlayerRecordID, OpenOnly: true})
|
||||
@@ -198,23 +198,55 @@ 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 validateGiftItems(version string, items []domain.GameGiftItem) error {
|
||||
if _, ok := domain.SCUMGiftCatalogForVersion(version); !ok {
|
||||
return validationError("SCUM game version has no verified gift item catalog")
|
||||
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")
|
||||
}
|
||||
if len(items) == 0 || len(items) > 8 {
|
||||
return validationError("gift requires 1 to 8 catalog items")
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
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 {
|
||||
for index, item := range items {
|
||||
def, ok := scumGiftItem(catalog, item.CatalogItemKey)
|
||||
if !ok || seen[item.CatalogItemKey] || item.Quantity < 1 || item.Quantity > def.MaximumQuantity {
|
||||
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,6 +100,7 @@ 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)
|
||||
}
|
||||
|
||||
@@ -296,7 +296,7 @@ func (svc *CoreService) upsertSCUMDataRow(target domain.SCUMRowTargetDeclaration
|
||||
|
||||
func validSCUMDataSet(value domain.SCUMDataSet) bool {
|
||||
switch value {
|
||||
case domain.SCUMDataSetUsers, domain.SCUMDataSetSquads, domain.SCUMDataSetMembers, domain.SCUMDataSetVehicles, domain.SCUMDataSetFlags, domain.SCUMDataSetActivity, domain.SCUMDataSetGifts, domain.SCUMDataSetMapPoints:
|
||||
case domain.SCUMDataSetUsers, domain.SCUMDataSetSquads, domain.SCUMDataSetMembers, domain.SCUMDataSetVehicles, domain.SCUMDataSetFlags, domain.SCUMDataSetActivity, domain.SCUMDataSetGiftEvents, domain.SCUMDataSetMapPoints:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
|
||||
@@ -782,7 +782,7 @@ func validateGameClientBridgeManifest(field string, bridge domain.GameClientBrid
|
||||
|
||||
func validSCUMTargetTable(value string) bool {
|
||||
switch value {
|
||||
case "scum_users", "scum_squads", "scum_activity_events", "scum_gift_catalogs", "scum_map_points":
|
||||
case "scum_users", "scum_squads", "scum_activity_events", "scum_gift_events", "scum_map_points":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
|
||||
Reference in New Issue
Block a user