Add SCUM log sessions and trajectory projections

This commit is contained in:
npc0-hue
2026-08-27 12:34:07 +08:00
parent 0940780058
commit 316efbe780
38 changed files with 1549 additions and 84 deletions
+205 -4
View File
@@ -167,7 +167,7 @@ func ValidateGamePlugin(plugin domain.GamePlugin) error {
}
violations = append(violations, validateRuntimeProfileCapabilityDeclarations(plugin.RuntimeProfiles, plugin.RequiredRunCapabilities)...)
violations = append(violations, validateRuntimeLogEventPermissionDeclarations("runtimeProfiles.logEvents", plugin.RuntimeProfiles, plugin.DeclaredPermissions)...)
violations = append(violations, validateGameClientBridgeManifest("gameClientBridge", plugin.GameClientBridge, plugin.DeclaredPermissions, plugin.Pages, plugin.RuntimeProfiles)...)
violations = append(violations, validateGameClientBridgeManifest("gameClientBridge", plugin.GameClientBridge, plugin.DeclaredPermissions, plugin.RequiredRunCapabilities, plugin.Pages, plugin.RuntimeProfiles)...)
violations = append(violations, validatePluginCreateFields("createFields", plugin.CreateFields)...)
violations = append(violations, validatePluginAssetFiles("lifecycleAssets", plugin.LifecycleAssets)...)
violations = append(violations, validateSafePluginStrings("gamePlugin", pluginSafeStrings(plugin))...)
@@ -238,7 +238,7 @@ func ValidateGamePluginManifestRegistration(registration domain.GamePluginManife
}
violations = append(violations, validateRuntimeProfileCapabilityDeclarations(manifest.RuntimeProfiles, manifest.Capabilities)...)
violations = append(violations, validateRuntimeLogEventPermissionDeclarations("manifest.runtimeProfiles.logEvents", manifest.RuntimeProfiles, manifest.Permissions)...)
violations = append(violations, validateGameClientBridgeManifest("manifest.gameClientBridge", manifest.GameClientBridge, manifest.Permissions, manifest.Pages, manifest.RuntimeProfiles)...)
violations = append(violations, validateGameClientBridgeManifest("manifest.gameClientBridge", manifest.GameClientBridge, manifest.Permissions, manifest.Capabilities, manifest.Pages, manifest.RuntimeProfiles)...)
violations = append(violations, validatePluginAssetFileDeclarations("manifest.assetFiles", manifest.AssetFiles)...)
violations = append(violations, validatePluginAssetFiles("assetFiles", registration.AssetFiles)...)
violations = append(violations, validateRegistrationAssetCoverage(registration.Manifest.AssetFiles, registration.AssetFiles)...)
@@ -467,9 +467,9 @@ func ValidatePluginCreateInputs(fields []domain.PluginCreateField, inputs map[st
return finish(violations)
}
func validateGameClientBridgeManifest(field string, bridge domain.GameClientBridgeManifest, permissions []string, pages []domain.GamePluginPage, runtimeProfiles domain.GamePluginRuntimeProfiles) []string {
func validateGameClientBridgeManifest(field string, bridge domain.GameClientBridgeManifest, permissions []string, runCapabilities []string, pages []domain.GamePluginPage, runtimeProfiles domain.GamePluginRuntimeProfiles) []string {
companionPresent := bridge.Companion != (domain.GameClientBridgeCompanionDeclaration{})
if len(bridge.Commands) == 0 && len(bridge.Snapshots) == 0 && len(bridge.QueryTemplates) == 0 && len(bridge.LogProjections) == 0 && len(bridge.DataPacks) == 0 && len(bridge.Pages) == 0 && len(bridge.Features) == 0 && bridge.Retention.KeepForSeconds == 0 && bridge.Retention.MaxRecords == 0 && !companionPresent {
if len(bridge.Commands) == 0 && len(bridge.Snapshots) == 0 && len(bridge.QueryTemplates) == 0 && len(bridge.LogProjections) == 0 && len(bridge.LifecycleProjections) == 0 && len(bridge.DataPacks) == 0 && len(bridge.Pages) == 0 && len(bridge.Features) == 0 && bridge.Retention.KeepForSeconds == 0 && bridge.Retention.MaxRecords == 0 && !companionPresent {
return nil
}
var violations []string
@@ -610,6 +610,12 @@ func validateGameClientBridgeManifest(field string, bridge domain.GameClientBrid
if template.SQLRef != "" && !safeRelativeSQLRef(template.SQLRef) {
violations = append(violations, prefix+".sqlRef must reference a package-relative SQL asset")
}
if len(template.Projections) > 4 {
violations = append(violations, prefix+".projections must not exceed 4 targets")
}
for projectionIndex, projection := range template.Projections {
violations = append(violations, validateGameClientBridgeQueryProjection(fmt.Sprintf("%s.projections[%d]", prefix, projectionIndex), projection)...)
}
transport, exists := transports[template.TransportKey]
if !exists {
violations = append(violations, prefix+".transportKey must reference a declared runtime transport profile")
@@ -634,6 +640,18 @@ func validateGameClientBridgeManifest(field string, bridge domain.GameClientBrid
logProjectionKeys[projection.Key] = struct{}{}
violations = append(violations, validateGameClientBridgeLogProjection(prefix, projection)...)
}
lifecycleProjectionKeys := map[string]struct{}{}
for index, projection := range bridge.LifecycleProjections {
prefix := fmt.Sprintf("%s.lifecycleProjections[%d]", field, index)
if !clientManagerIdentifierPattern.MatchString(projection.Key) {
violations = append(violations, prefix+".key is invalid")
}
if _, exists := lifecycleProjectionKeys[projection.Key]; exists {
violations = append(violations, prefix+".key is duplicated")
}
lifecycleProjectionKeys[projection.Key] = struct{}{}
violations = append(violations, validateGameClientBridgeLifecycleProjection(prefix, projection, runCapabilities)...)
}
dataPackKeys := map[string]struct{}{}
for index, dataPack := range bridge.DataPacks {
prefix := fmt.Sprintf("%s.dataPacks[%d]", field, index)
@@ -753,6 +771,174 @@ func validateGameClientBridgeManifest(field string, bridge domain.GameClientBrid
return violations
}
func validateGameClientBridgeQueryProjection(prefix string, projection domain.GameClientBridgeQueryProjectionDeclaration) []string {
var violations []string
if !gameClientBridgeCollectionPattern.MatchString(projection.Collection) {
violations = append(violations, prefix+".collection is invalid")
}
if projection.RowPath != "rows" {
violations = append(violations, prefix+".rowPath must be rows")
}
if projection.MatchField != "" && !gameClientBridgeFieldPattern.MatchString(projection.MatchField) {
violations = append(violations, prefix+".matchField is invalid")
}
if projection.MatchField == "" && projection.MatchValue != "" || projection.MatchField != "" && strings.TrimSpace(projection.MatchValue) == "" {
violations = append(violations, prefix+".matchField and matchValue must be declared together")
}
if len([]rune(projection.MatchValue)) > 120 {
violations = append(violations, prefix+".matchValue is too long")
}
if len(projection.UpsertKeys) < 1 || len(projection.UpsertKeys) > 8 {
violations = append(violations, prefix+".upsertKeys must contain between 1 and 8 fields")
}
projectedFields := map[string]struct{}{}
for destination, source := range projection.FieldMappings {
if !gameClientBridgeFieldPattern.MatchString(destination) || !gameClientBridgeFieldPattern.MatchString(source) {
violations = append(violations, prefix+".fieldMappings contains an invalid field")
}
projectedFields[destination] = struct{}{}
}
if len(projection.FixedValues) > 64 {
violations = append(violations, prefix+".fixedValues contains too many fields")
}
for destination, value := range projection.FixedValues {
if !gameClientBridgeFieldPattern.MatchString(destination) || len([]rune(value)) > 4096 {
violations = append(violations, prefix+".fixedValues contains an invalid field or oversized value")
}
if _, exists := projectedFields[destination]; exists {
violations = append(violations, prefix+" declares field "+destination+" more than once")
}
projectedFields[destination] = struct{}{}
}
if projection.ObservedAtField != "" {
if !gameClientBridgeFieldPattern.MatchString(projection.ObservedAtField) {
violations = append(violations, prefix+".observedAtField is invalid")
}
if _, exists := projectedFields[projection.ObservedAtField]; exists {
violations = append(violations, prefix+" declares field "+projection.ObservedAtField+" more than once")
}
projectedFields[projection.ObservedAtField] = struct{}{}
}
for _, key := range projection.UpsertKeys {
if !gameClientBridgeFieldPattern.MatchString(key) {
violations = append(violations, prefix+".upsertKeys contains an invalid field")
}
if len(projection.FieldMappings) > 0 {
if _, exists := projectedFields[key]; !exists {
violations = append(violations, prefix+".upsertKeys field "+key+" is not projected")
}
}
}
violations = append(violations, duplicateViolations(prefix+".upsertKeys", projection.UpsertKeys)...)
return violations
}
func validateGameClientBridgeLifecycleProjection(prefix string, projection domain.GameClientBridgeLifecycleProjectionDeclaration, pluginRunCapabilities []string) []string {
var violations []string
if len(projection.Capabilities) < 1 || len(projection.Capabilities) > 16 {
violations = append(violations, prefix+".capabilities must contain between 1 and 16 values")
}
for _, capability := range projection.Capabilities {
if !validPluginRunCapability(capability) {
violations = append(violations, prefix+".capabilities contains an invalid capability")
}
if !containsString(pluginRunCapabilities, capability) {
violations = append(violations, prefix+".capabilities must be declared by the plugin")
}
}
violations = append(violations, duplicateViolations(prefix+".capabilities", projection.Capabilities)...)
if len(projection.ProcessStates) > 8 {
violations = append(violations, prefix+".processStates must not exceed 8")
}
for _, state := range projection.ProcessStates {
if !oneOf(state, "running", "stopped", "not-started", "exited") {
violations = append(violations, prefix+".processStates contains an invalid process state")
}
}
violations = append(violations, duplicateViolations(prefix+".processStates", projection.ProcessStates)...)
violations = append(violations, validateGameClientBridgeBulkProjectionTarget(prefix+".target", projection.Target)...)
return violations
}
func validateGameClientBridgeBulkProjectionTarget(prefix string, target domain.GameClientBridgeBulkProjectionTargetDeclaration) []string {
var violations []string
if !gameClientBridgeCollectionPattern.MatchString(target.Collection) {
violations = append(violations, prefix+".collection is invalid")
}
if !gameClientBridgeFieldPattern.MatchString(target.MatchField) {
violations = append(violations, prefix+".matchField is invalid")
}
if strings.TrimSpace(target.MatchValue) == "" || len([]rune(target.MatchValue)) > 120 {
violations = append(violations, prefix+".matchValue is invalid")
}
if len(target.FixedValues) < 1 || len(target.FixedValues) > 64 {
violations = append(violations, prefix+".fixedValues must contain between 1 and 64 fields")
}
for destination, value := range target.FixedValues {
if !gameClientBridgeFieldPattern.MatchString(destination) || len([]rune(value)) > 4096 {
violations = append(violations, prefix+".fixedValues contains an invalid field or oversized value")
}
}
if target.ObservedAtField != "" && !gameClientBridgeFieldPattern.MatchString(target.ObservedAtField) {
violations = append(violations, prefix+".observedAtField is invalid")
}
if target.ActivityTarget != nil {
violations = append(violations, validateGameClientBridgeBulkActivityTarget(prefix+".activityTarget", *target.ActivityTarget)...)
}
return violations
}
func validateGameClientBridgeBulkActivityTarget(prefix string, target domain.GameClientBridgeBulkActivityTargetDeclaration) []string {
var violations []string
if !gameClientBridgeCollectionPattern.MatchString(target.Collection) {
violations = append(violations, prefix+".collection is invalid")
}
if len(target.RowMappings) < 1 || len(target.RowMappings) > 64 {
violations = append(violations, prefix+".rowMappings must contain between 1 and 64 mappings")
}
projectedFields := map[string]struct{}{}
for destination, source := range target.RowMappings {
if !gameClientBridgeFieldPattern.MatchString(destination) || !gameClientBridgeFieldPattern.MatchString(source) {
violations = append(violations, prefix+".rowMappings contains an invalid field")
}
projectedFields[destination] = struct{}{}
}
if len(target.FixedValues) > 64 {
violations = append(violations, prefix+".fixedValues contains too many fields")
}
for destination, value := range target.FixedValues {
if !gameClientBridgeFieldPattern.MatchString(destination) || len([]rune(value)) > 4096 {
violations = append(violations, prefix+".fixedValues contains an invalid field or oversized value")
}
if _, exists := projectedFields[destination]; exists {
violations = append(violations, prefix+" declares field "+destination+" more than once")
}
projectedFields[destination] = struct{}{}
}
if target.ObservedAtField != "" {
if !gameClientBridgeFieldPattern.MatchString(target.ObservedAtField) {
violations = append(violations, prefix+".observedAtField is invalid")
}
if _, exists := projectedFields[target.ObservedAtField]; exists {
violations = append(violations, prefix+" declares field "+target.ObservedAtField+" more than once")
}
projectedFields[target.ObservedAtField] = struct{}{}
}
if len(target.UpsertKeys) < 1 || len(target.UpsertKeys) > 8 {
violations = append(violations, prefix+".upsertKeys must contain between 1 and 8 fields")
}
for _, key := range target.UpsertKeys {
if !gameClientBridgeFieldPattern.MatchString(key) {
violations = append(violations, prefix+".upsertKeys contains an invalid field")
}
if _, exists := projectedFields[key]; !exists {
violations = append(violations, prefix+".upsertKeys field "+key+" is not projected")
}
}
violations = append(violations, duplicateViolations(prefix+".upsertKeys", target.UpsertKeys)...)
return violations
}
func validateGameClientBridgeLogProjection(prefix string, projection domain.GameClientBridgeLogProjectionDeclaration) []string {
var violations []string
if len(projection.StreamKeys) < 1 || len(projection.StreamKeys) > 64 {
@@ -852,6 +1038,21 @@ func validateGameClientBridgeLogProjectionTarget(prefix string, target domain.Ga
}
projectedFields[destination] = struct{}{}
}
if len(target.HashMappings) > 64 {
violations = append(violations, prefix+".hashMappings contains too many fields")
}
for destination, capture := range target.HashMappings {
if !gameClientBridgeFieldPattern.MatchString(destination) || !gameClientBridgeCaptureNamePattern.MatchString(capture) {
violations = append(violations, prefix+".hashMappings contains an invalid field or capture")
}
if _, exists := captures[capture]; !exists {
violations = append(violations, prefix+".hashMappings references undeclared capture "+capture)
}
if _, exists := projectedFields[destination]; exists {
violations = append(violations, prefix+" declares field "+destination+" more than once")
}
projectedFields[destination] = struct{}{}
}
if len(target.FixedValues) > 64 {
violations = append(violations, prefix+".fixedValues contains too many fields")
}