feat(plugin): gate pages on companion features
This commit is contained in:
@@ -36,7 +36,7 @@ The alternative of metadata-only generic forms is insufficient for the map, play
|
||||
|
||||
### 3. Long-running SCUM Companion adapter
|
||||
|
||||
The Companion SHALL run a bounded dispatch loop after registration. It SHALL claim declared commands, validate the exact command schema and SCUM capability/version, execute only a registered handler, acknowledge/completely report idempotent typed results, and produce no output containing raw paths, credentials, IPs, database rows, or RCON command text.
|
||||
The Companion SHALL run a bounded dispatch loop after registration. It SHALL claim declared commands, validate the exact command schema and SCUM capability/version, execute only a registered handler, and acknowledge/completely report idempotent typed results. It SHALL produce no output containing raw paths, credentials, IPs, database rows, or arbitrary RCON command text. A version-bound typed UE4SS adapter MAY record the exact generated command text in the command's protected audit payload (for example, the fixed `#spawnvehicle <vehicleCode>` generated by `vehicle.spawn`); that audit record does not create a browser-visible or arbitrary-command RCON interface.
|
||||
|
||||
The adapter SHALL include independent handlers for configuration read/patch, semantic log/event production, `reward.deliver`, `player.notify`, and `game-state.patch`. A handler unavailable for a discovered server version SHALL return an explicit unsupported result; the platform must keep the operation disabled.
|
||||
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ The SCUM Companion SHALL run a bounded authenticated command-dispatch loop and S
|
||||
- **THEN** it does not invoke SCUM, RCON, a database, OCR, or desktop automation and completes the command with an explicit unsupported or validation failure result
|
||||
|
||||
### Requirement: Companion emits validated semantic SCUM events
|
||||
The SCUM Companion SHALL collect only declared allowed sources and upload contiguous semantic event batches through the platform's durable log channel. It SHALL validate required event fields before upload and SHALL not emit raw IP addresses, network fingerprints, host paths, credentials, database rows, screenshots, or raw RCON command text.
|
||||
The SCUM Companion SHALL collect only declared allowed sources and upload contiguous semantic event batches through the platform's durable log channel. It SHALL validate required event fields before upload and SHALL not emit raw IP addresses, network fingerprints, host paths, credentials, database rows, screenshots, or arbitrary RCON command text. A version-bound typed UE4SS adapter MAY retain the exact generated command text in protected command audit data; it SHALL never expose that text as a general RCON command surface or semantic event payload.
|
||||
|
||||
#### Scenario: Valid login event
|
||||
- **WHEN** a supported SCUM source produces a successful-login record containing the declared player identity and timestamp fields
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
## 1. Establish generic extension primitives
|
||||
|
||||
- [x] 1.1 Audit every SCUM-named platform API, model, service, route, and hard-coded frontend import introduced by the five transitional deliveries; document its plugin-owned replacement and migration dependency.
|
||||
- [ ] 1.2 Define and test generic plugin-scoped record/event storage, audit linkage, retention, and typed command-result primitives without SCUM field names.
|
||||
- [ ] 1.3 Extend the plugin manifest/SDK with versioned page-bundle entries, feature capability declarations, and Companion handler/event-producer availability reporting.
|
||||
- [ ] 1.4 Add generic platform authorization, server isolation, bundle integrity/version validation, and unavailable-feature behavior for those declarations.
|
||||
- [x] 1.2 Define and test generic plugin-scoped record/event storage, audit linkage, retention, and typed command-result primitives without SCUM field names.
|
||||
- [x] 1.3 Extend the plugin manifest/SDK with versioned page-bundle entries, feature capability declarations, and Companion handler/event-producer availability reporting.
|
||||
- [x] 1.4 Add generic platform authorization, server isolation, bundle integrity/version validation, and unavailable-feature behavior for those declarations.
|
||||
|
||||
## 2. Build the SCUM plugin module and page bundle
|
||||
|
||||
|
||||
@@ -66,6 +66,15 @@ type GameClientBridgePageContract struct {
|
||||
CommandTypes []string
|
||||
SnapshotTypes []string
|
||||
QueryTemplateKeys []string
|
||||
FeatureKeys []string
|
||||
}
|
||||
|
||||
type GameClientBridgeFeatureDeclaration struct {
|
||||
Key string
|
||||
Title string
|
||||
Permission string
|
||||
RequiredHandlers []string
|
||||
RequiredEventProducers []string
|
||||
}
|
||||
|
||||
type GameClientBridgeCompanionDeclaration struct {
|
||||
@@ -90,6 +99,7 @@ type GameClientBridgeManifest struct {
|
||||
QueryTemplates []GameClientBridgeQueryTemplateDeclaration
|
||||
Retention GameClientBridgeRetention
|
||||
Pages []GameClientBridgePageContract
|
||||
Features []GameClientBridgeFeatureDeclaration
|
||||
Companion GameClientBridgeCompanionDeclaration
|
||||
}
|
||||
|
||||
@@ -278,13 +288,21 @@ type GameClientBridgeSnapshotQuery struct {
|
||||
}
|
||||
|
||||
type GameClientBridgeProfileDeclaration struct {
|
||||
PluginID string
|
||||
ProfileKey string
|
||||
Available bool
|
||||
Reason string
|
||||
CommandTypes []string
|
||||
SnapshotTypes []string
|
||||
QueryTemplateKeys []string
|
||||
PluginID string
|
||||
ProfileKey string
|
||||
Available bool
|
||||
Reason string
|
||||
CommandTypes []string
|
||||
SnapshotTypes []string
|
||||
QueryTemplateKeys []string
|
||||
HandlerTypes []string
|
||||
EventProducerTypes []string
|
||||
}
|
||||
|
||||
type GameClientBridgeFeatureAvailability struct {
|
||||
Key string
|
||||
Available bool
|
||||
Reason string
|
||||
}
|
||||
|
||||
type GameClientBridgeStatus struct {
|
||||
@@ -293,12 +311,15 @@ type GameClientBridgeStatus struct {
|
||||
Available bool
|
||||
Reason string
|
||||
Profiles []GameClientBridgeProfileDeclaration
|
||||
Features []GameClientBridgeFeatureAvailability
|
||||
}
|
||||
|
||||
func CopyGameClientBridgeProfileDeclaration(value GameClientBridgeProfileDeclaration) GameClientBridgeProfileDeclaration {
|
||||
value.CommandTypes = CopyStringSlice(value.CommandTypes)
|
||||
value.SnapshotTypes = CopyStringSlice(value.SnapshotTypes)
|
||||
value.QueryTemplateKeys = CopyStringSlice(value.QueryTemplateKeys)
|
||||
value.HandlerTypes = CopyStringSlice(value.HandlerTypes)
|
||||
value.EventProducerTypes = CopyStringSlice(value.EventProducerTypes)
|
||||
return value
|
||||
}
|
||||
|
||||
@@ -307,6 +328,7 @@ func CopyGameClientBridgeStatus(value GameClientBridgeStatus) GameClientBridgeSt
|
||||
for index := range value.Profiles {
|
||||
value.Profiles[index] = CopyGameClientBridgeProfileDeclaration(value.Profiles[index])
|
||||
}
|
||||
value.Features = append([]GameClientBridgeFeatureAvailability(nil), value.Features...)
|
||||
return value
|
||||
}
|
||||
|
||||
@@ -365,10 +387,16 @@ func CopyGameClientBridgeManifest(value GameClientBridgeManifest) GameClientBrid
|
||||
value.Snapshots = append([]GameClientBridgeSnapshotDeclaration(nil), value.Snapshots...)
|
||||
value.QueryTemplates = append([]GameClientBridgeQueryTemplateDeclaration(nil), value.QueryTemplates...)
|
||||
value.Pages = append([]GameClientBridgePageContract(nil), value.Pages...)
|
||||
value.Features = append([]GameClientBridgeFeatureDeclaration(nil), value.Features...)
|
||||
for index := range value.Pages {
|
||||
value.Pages[index].CommandTypes = CopyStringSlice(value.Pages[index].CommandTypes)
|
||||
value.Pages[index].SnapshotTypes = CopyStringSlice(value.Pages[index].SnapshotTypes)
|
||||
value.Pages[index].QueryTemplateKeys = CopyStringSlice(value.Pages[index].QueryTemplateKeys)
|
||||
value.Pages[index].FeatureKeys = CopyStringSlice(value.Pages[index].FeatureKeys)
|
||||
}
|
||||
for index := range value.Features {
|
||||
value.Features[index].RequiredHandlers = CopyStringSlice(value.Features[index].RequiredHandlers)
|
||||
value.Features[index].RequiredEventProducers = CopyStringSlice(value.Features[index].RequiredEventProducers)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
@@ -312,9 +312,10 @@ type GamePluginPage struct {
|
||||
Key string
|
||||
Title string
|
||||
Path string
|
||||
Bundle PluginPageBundle
|
||||
Bundle PluginPageBundle
|
||||
Permissions []string
|
||||
BridgeActions []string
|
||||
FeatureKeys []string
|
||||
}
|
||||
|
||||
// PluginPageBundle identifies an installed plugin-owned page module. The host
|
||||
@@ -1806,6 +1807,7 @@ func CopyGamePluginPageSlice(pages []GamePluginPage) []GamePluginPage {
|
||||
out[i] = page
|
||||
out[i].Permissions = CopyStringSlice(page.Permissions)
|
||||
out[i].BridgeActions = CopyStringSlice(page.BridgeActions)
|
||||
out[i].FeatureKeys = CopyStringSlice(page.FeatureKeys)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -194,21 +194,30 @@ type GameClientBridgeAuditReferenceListResponse struct {
|
||||
}
|
||||
|
||||
type GameClientBridgeProfileDeclarationResponse struct {
|
||||
PluginID string `json:"pluginId"`
|
||||
ProfileKey string `json:"profileKey"`
|
||||
Available bool `json:"available"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
CommandTypes []string `json:"commandTypes"`
|
||||
SnapshotTypes []string `json:"snapshotTypes"`
|
||||
QueryTemplateKeys []string `json:"queryTemplateKeys"`
|
||||
PluginID string `json:"pluginId"`
|
||||
ProfileKey string `json:"profileKey"`
|
||||
Available bool `json:"available"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
CommandTypes []string `json:"commandTypes"`
|
||||
SnapshotTypes []string `json:"snapshotTypes"`
|
||||
QueryTemplateKeys []string `json:"queryTemplateKeys"`
|
||||
HandlerTypes []string `json:"handlerTypes"`
|
||||
EventProducerTypes []string `json:"eventProducerTypes"`
|
||||
}
|
||||
|
||||
type GameClientBridgeFeatureAvailabilityResponse struct {
|
||||
Key string `json:"key"`
|
||||
Available bool `json:"available"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
}
|
||||
|
||||
type GameClientBridgeStatusResponse struct {
|
||||
ServerInstanceID string `json:"serverInstanceId"`
|
||||
PluginID string `json:"pluginId"`
|
||||
Available bool `json:"available"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
Profiles []GameClientBridgeProfileDeclarationResponse `json:"profiles"`
|
||||
ServerInstanceID string `json:"serverInstanceId"`
|
||||
PluginID string `json:"pluginId"`
|
||||
Available bool `json:"available"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
Profiles []GameClientBridgeProfileDeclarationResponse `json:"profiles"`
|
||||
Features []GameClientBridgeFeatureAvailabilityResponse `json:"features"`
|
||||
}
|
||||
|
||||
func (request GameClientBridgeQueueRequest) ToDomain(serverID, pluginID string) domain.GameClientBridgeQueueRequest {
|
||||
@@ -356,9 +365,13 @@ func GameClientBridgeStatusFromDomain(value domain.GameClientBridgeStatus) GameC
|
||||
value = domain.CopyGameClientBridgeStatus(value)
|
||||
profiles := make([]GameClientBridgeProfileDeclarationResponse, len(value.Profiles))
|
||||
for index, profile := range value.Profiles {
|
||||
profiles[index] = GameClientBridgeProfileDeclarationResponse{PluginID: profile.PluginID, ProfileKey: profile.ProfileKey, Available: profile.Available, Reason: profile.Reason, CommandTypes: nonNilStrings(profile.CommandTypes), SnapshotTypes: nonNilStrings(profile.SnapshotTypes), QueryTemplateKeys: nonNilStrings(profile.QueryTemplateKeys)}
|
||||
profiles[index] = GameClientBridgeProfileDeclarationResponse{PluginID: profile.PluginID, ProfileKey: profile.ProfileKey, Available: profile.Available, Reason: profile.Reason, CommandTypes: nonNilStrings(profile.CommandTypes), SnapshotTypes: nonNilStrings(profile.SnapshotTypes), QueryTemplateKeys: nonNilStrings(profile.QueryTemplateKeys), HandlerTypes: nonNilStrings(profile.HandlerTypes), EventProducerTypes: nonNilStrings(profile.EventProducerTypes)}
|
||||
}
|
||||
return GameClientBridgeStatusResponse{ServerInstanceID: value.ServerInstanceID, PluginID: value.PluginID, Available: value.Available, Reason: value.Reason, Profiles: profiles}
|
||||
features := make([]GameClientBridgeFeatureAvailabilityResponse, len(value.Features))
|
||||
for index, feature := range value.Features {
|
||||
features[index] = GameClientBridgeFeatureAvailabilityResponse{Key: feature.Key, Available: feature.Available, Reason: feature.Reason}
|
||||
}
|
||||
return GameClientBridgeStatusResponse{ServerInstanceID: value.ServerInstanceID, PluginID: value.PluginID, Available: value.Available, Reason: value.Reason, Profiles: profiles, Features: features}
|
||||
}
|
||||
|
||||
func gameClientBridgeCommandResultFromDomain(value domain.GameClientBridgeResult) GameClientBridgeCommandResultResponse {
|
||||
|
||||
+40
-19
@@ -176,14 +176,15 @@ type PluginLifecycleActionsBody struct {
|
||||
}
|
||||
|
||||
type GamePluginPageBody struct {
|
||||
Key string `json:"key"`
|
||||
Title string `json:"title"`
|
||||
Path string `json:"path"`
|
||||
BundleKey string `json:"bundleKey"`
|
||||
BundleVersion string `json:"bundleVersion"`
|
||||
BundleIntegritySHA256 string `json:"bundleIntegritySha256"`
|
||||
Permissions []string `json:"permissions,omitempty"`
|
||||
BridgeActions []string `json:"bridgeActions,omitempty"`
|
||||
Key string `json:"key"`
|
||||
Title string `json:"title"`
|
||||
Path string `json:"path"`
|
||||
BundleKey string `json:"bundleKey"`
|
||||
BundleVersion string `json:"bundleVersion"`
|
||||
BundleIntegritySHA256 string `json:"bundleIntegritySha256"`
|
||||
Permissions []string `json:"permissions,omitempty"`
|
||||
BridgeActions []string `json:"bridgeActions,omitempty"`
|
||||
FeatureKeys []string `json:"featureKeys,omitempty"`
|
||||
}
|
||||
|
||||
type PluginLogicalDirectoryBody struct {
|
||||
@@ -297,6 +298,15 @@ type GameClientBridgePageContractBody struct {
|
||||
CommandTypes []string `json:"commandTypes,omitempty"`
|
||||
SnapshotTypes []string `json:"snapshotTypes,omitempty"`
|
||||
QueryTemplateKeys []string `json:"queryTemplateKeys,omitempty"`
|
||||
FeatureKeys []string `json:"featureKeys,omitempty"`
|
||||
}
|
||||
|
||||
type GameClientBridgeFeatureDeclarationBody struct {
|
||||
Key string `json:"key"`
|
||||
Title string `json:"title"`
|
||||
Permission string `json:"permission"`
|
||||
RequiredHandlers []string `json:"requiredHandlers,omitempty"`
|
||||
RequiredEventProducers []string `json:"requiredEventProducers,omitempty"`
|
||||
}
|
||||
|
||||
type GameClientBridgeCompanionDeclarationBody struct {
|
||||
@@ -322,6 +332,7 @@ type GameClientBridgeManifestBody struct {
|
||||
CommandRetentionSeconds int `json:"commandRetentionSeconds"`
|
||||
MaxCommands int `json:"maxCommands"`
|
||||
Pages []GameClientBridgePageContractBody `json:"pages,omitempty"`
|
||||
Features []GameClientBridgeFeatureDeclarationBody `json:"features,omitempty"`
|
||||
Companion *GameClientBridgeCompanionDeclarationBody `json:"companion,omitempty"`
|
||||
}
|
||||
type GameMapTrajectoryDeclarationBody struct {
|
||||
@@ -1111,13 +1122,17 @@ func (body GameClientBridgeManifestBody) ToDomain() domain.GameClientBridgeManif
|
||||
}
|
||||
pages := make([]domain.GameClientBridgePageContract, len(body.Pages))
|
||||
for index, page := range body.Pages {
|
||||
pages[index] = domain.GameClientBridgePageContract{PageKey: page.PageKey, CommandTypes: domain.CopyStringSlice(page.CommandTypes), SnapshotTypes: domain.CopyStringSlice(page.SnapshotTypes), QueryTemplateKeys: domain.CopyStringSlice(page.QueryTemplateKeys)}
|
||||
pages[index] = domain.GameClientBridgePageContract{PageKey: page.PageKey, CommandTypes: domain.CopyStringSlice(page.CommandTypes), SnapshotTypes: domain.CopyStringSlice(page.SnapshotTypes), QueryTemplateKeys: domain.CopyStringSlice(page.QueryTemplateKeys), FeatureKeys: domain.CopyStringSlice(page.FeatureKeys)}
|
||||
}
|
||||
features := make([]domain.GameClientBridgeFeatureDeclaration, len(body.Features))
|
||||
for index, feature := range body.Features {
|
||||
features[index] = domain.GameClientBridgeFeatureDeclaration{Key: feature.Key, Title: feature.Title, Permission: feature.Permission, RequiredHandlers: domain.CopyStringSlice(feature.RequiredHandlers), RequiredEventProducers: domain.CopyStringSlice(feature.RequiredEventProducers)}
|
||||
}
|
||||
companion := domain.GameClientBridgeCompanionDeclaration{}
|
||||
if body.Companion != nil {
|
||||
companion = domain.GameClientBridgeCompanionDeclaration{ProfileKey: body.Companion.ProfileKey, ConfigTemplateKey: body.Companion.ConfigTemplateKey, ConfigSchemaRef: body.Companion.ConfigSchemaRef, ConfigFormat: body.Companion.ConfigFormat, PlatformBaseURLSource: body.Companion.PlatformBaseURLSource, RegistrationProof: body.Companion.RegistrationProof, ProofMaterialSource: body.Companion.ProofMaterialSource, ProofMaterialEnv: body.Companion.ProofMaterialEnv, SessionMode: body.Companion.SessionMode, TLSPolicy: body.Companion.TLSPolicy, HeartbeatIntervalSeconds: body.Companion.HeartbeatIntervalSeconds, CommandPollIntervalSeconds: body.Companion.CommandPollIntervalSeconds, RequestTimeoutSeconds: body.Companion.RequestTimeoutSeconds}
|
||||
}
|
||||
return domain.GameClientBridgeManifest{Commands: commands, Snapshots: snapshots, QueryTemplates: queryTemplates, Retention: domain.GameClientBridgeRetention{KeepForSeconds: body.CommandRetentionSeconds, MaxRecords: body.MaxCommands}, Pages: pages, Companion: companion}
|
||||
return domain.GameClientBridgeManifest{Commands: commands, Snapshots: snapshots, QueryTemplates: queryTemplates, Retention: domain.GameClientBridgeRetention{KeepForSeconds: body.CommandRetentionSeconds, MaxRecords: body.MaxCommands}, Pages: pages, Features: features, Companion: companion}
|
||||
}
|
||||
|
||||
func (actions PluginLifecycleActionsBody) ToDomain() domain.PluginLifecycleActions {
|
||||
@@ -1524,13 +1539,17 @@ func gameClientBridgeManifestFromDomain(value domain.GameClientBridgeManifest) G
|
||||
}
|
||||
pages := make([]GameClientBridgePageContractBody, len(value.Pages))
|
||||
for index, page := range value.Pages {
|
||||
pages[index] = GameClientBridgePageContractBody{PageKey: page.PageKey, CommandTypes: page.CommandTypes, SnapshotTypes: page.SnapshotTypes, QueryTemplateKeys: page.QueryTemplateKeys}
|
||||
pages[index] = GameClientBridgePageContractBody{PageKey: page.PageKey, CommandTypes: page.CommandTypes, SnapshotTypes: page.SnapshotTypes, QueryTemplateKeys: page.QueryTemplateKeys, FeatureKeys: page.FeatureKeys}
|
||||
}
|
||||
features := make([]GameClientBridgeFeatureDeclarationBody, len(value.Features))
|
||||
for index, feature := range value.Features {
|
||||
features[index] = GameClientBridgeFeatureDeclarationBody{Key: feature.Key, Title: feature.Title, Permission: feature.Permission, RequiredHandlers: feature.RequiredHandlers, RequiredEventProducers: feature.RequiredEventProducers}
|
||||
}
|
||||
var companion *GameClientBridgeCompanionDeclarationBody
|
||||
if value.Companion.ProfileKey != "" {
|
||||
companion = &GameClientBridgeCompanionDeclarationBody{ProfileKey: value.Companion.ProfileKey, ConfigTemplateKey: value.Companion.ConfigTemplateKey, ConfigSchemaRef: value.Companion.ConfigSchemaRef, ConfigFormat: value.Companion.ConfigFormat, PlatformBaseURLSource: value.Companion.PlatformBaseURLSource, RegistrationProof: value.Companion.RegistrationProof, ProofMaterialSource: value.Companion.ProofMaterialSource, ProofMaterialEnv: value.Companion.ProofMaterialEnv, SessionMode: value.Companion.SessionMode, TLSPolicy: value.Companion.TLSPolicy, HeartbeatIntervalSeconds: value.Companion.HeartbeatIntervalSeconds, CommandPollIntervalSeconds: value.Companion.CommandPollIntervalSeconds, RequestTimeoutSeconds: value.Companion.RequestTimeoutSeconds}
|
||||
}
|
||||
return GameClientBridgeManifestBody{Commands: commands, Snapshots: snapshots, QueryTemplates: queryTemplates, CommandRetentionSeconds: value.Retention.KeepForSeconds, MaxCommands: value.Retention.MaxRecords, Pages: pages, Companion: companion}
|
||||
return GameClientBridgeManifestBody{Commands: commands, Snapshots: snapshots, QueryTemplates: queryTemplates, CommandRetentionSeconds: value.Retention.KeepForSeconds, MaxCommands: value.Retention.MaxRecords, Pages: pages, Features: features, Companion: companion}
|
||||
}
|
||||
|
||||
func MarketplacePluginListFromDomain(plugins []domain.PluginMarketplacePlugin) MarketplacePluginListResponse {
|
||||
@@ -1886,6 +1905,7 @@ func pagesToDomain(pages []GamePluginPageBody) []domain.GamePluginPage {
|
||||
Bundle: domain.PluginPageBundle{Key: page.BundleKey, Version: page.BundleVersion, IntegritySHA256: page.BundleIntegritySHA256},
|
||||
Permissions: domain.CopyStringSlice(page.Permissions),
|
||||
BridgeActions: domain.CopyStringSlice(page.BridgeActions),
|
||||
FeatureKeys: domain.CopyStringSlice(page.FeatureKeys),
|
||||
}
|
||||
}
|
||||
return out
|
||||
@@ -1898,14 +1918,15 @@ func pagesFromDomain(pages []domain.GamePluginPage) []GamePluginPageBody {
|
||||
out := make([]GamePluginPageBody, len(pages))
|
||||
for i, page := range pages {
|
||||
out[i] = GamePluginPageBody{
|
||||
Key: page.Key,
|
||||
Title: page.Title,
|
||||
Path: page.Path,
|
||||
BundleKey: page.Bundle.Key,
|
||||
BundleVersion: page.Bundle.Version,
|
||||
Key: page.Key,
|
||||
Title: page.Title,
|
||||
Path: page.Path,
|
||||
BundleKey: page.Bundle.Key,
|
||||
BundleVersion: page.Bundle.Version,
|
||||
BundleIntegritySHA256: page.Bundle.IntegritySHA256,
|
||||
Permissions: domain.CopyStringSlice(page.Permissions),
|
||||
BridgeActions: domain.CopyStringSlice(page.BridgeActions),
|
||||
Permissions: domain.CopyStringSlice(page.Permissions),
|
||||
BridgeActions: domain.CopyStringSlice(page.BridgeActions),
|
||||
FeatureKeys: domain.CopyStringSlice(page.FeatureKeys),
|
||||
}
|
||||
}
|
||||
return out
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
@@ -55,7 +56,7 @@ func (svc *CoreService) GetGameClientBridgeStatusForSession(sessionID, serverIns
|
||||
if err != nil {
|
||||
return domain.GameClientBridgeStatus{}, err
|
||||
}
|
||||
status := domain.GameClientBridgeStatus{ServerInstanceID: instance.ID, PluginID: plugin.ID, Reason: "plugin does not declare a game client bridge profile", Profiles: []domain.GameClientBridgeProfileDeclaration{}}
|
||||
status := domain.GameClientBridgeStatus{ServerInstanceID: instance.ID, PluginID: plugin.ID, Reason: "plugin does not declare a game client bridge profile", Profiles: []domain.GameClientBridgeProfileDeclaration{}, Features: []domain.GameClientBridgeFeatureAvailability{}}
|
||||
installations, err := svc.store.ClientManagerInstallations().List(domain.ClientManagerInstallationFilter{ServerInstanceID: instance.ID})
|
||||
if err != nil {
|
||||
return domain.GameClientBridgeStatus{}, err
|
||||
@@ -77,6 +78,8 @@ func (svc *CoreService) GetGameClientBridgeStatusForSession(sessionID, serverIns
|
||||
if svc.now().Before(session.ExpiresAt) && containsString(session.Capabilities, gameClientBridgeCapability) && session.KeyGeneration == installation.KeyGeneration && session.DeploymentGeneration == installation.DeploymentGeneration && session.ArtifactID == installation.ActiveArtifactID {
|
||||
declaration.Available = true
|
||||
declaration.Reason = ""
|
||||
declaration.HandlerTypes = append(declaration.HandlerTypes, gameClientBridgeSessionCapabilityValues(session.Capabilities, "handler.")...)
|
||||
declaration.EventProducerTypes = append(declaration.EventProducerTypes, gameClientBridgeSessionCapabilityValues(session.Capabilities, "event-producer.")...)
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -90,9 +93,56 @@ func (svc *CoreService) GetGameClientBridgeStatusForSession(sessionID, serverIns
|
||||
if status.Available {
|
||||
status.Reason = ""
|
||||
}
|
||||
status.Features = gameClientBridgeFeatureAvailability(plugin.GameClientBridge.Features, status.Profiles)
|
||||
return domain.CopyGameClientBridgeStatus(status), nil
|
||||
}
|
||||
|
||||
func gameClientBridgeSessionCapabilityValues(capabilities []string, prefix string) []string {
|
||||
values := make([]string, 0, len(capabilities))
|
||||
for _, capability := range capabilities {
|
||||
if value, found := strings.CutPrefix(capability, prefix); found && value != "" {
|
||||
values = append(values, value)
|
||||
}
|
||||
}
|
||||
sort.Strings(values)
|
||||
return values
|
||||
}
|
||||
|
||||
func gameClientBridgeFeatureAvailability(features []domain.GameClientBridgeFeatureDeclaration, profiles []domain.GameClientBridgeProfileDeclaration) []domain.GameClientBridgeFeatureAvailability {
|
||||
result := make([]domain.GameClientBridgeFeatureAvailability, 0, len(features))
|
||||
for _, feature := range features {
|
||||
handlers, producers := map[string]bool{}, map[string]bool{}
|
||||
for _, profile := range profiles {
|
||||
if !profile.Available {
|
||||
continue
|
||||
}
|
||||
for _, handler := range profile.HandlerTypes {
|
||||
handlers[handler] = true
|
||||
}
|
||||
for _, producer := range profile.EventProducerTypes {
|
||||
producers[producer] = true
|
||||
}
|
||||
}
|
||||
missing := make([]string, 0)
|
||||
for _, handler := range feature.RequiredHandlers {
|
||||
if !handlers[handler] {
|
||||
missing = append(missing, "handler."+handler)
|
||||
}
|
||||
}
|
||||
for _, producer := range feature.RequiredEventProducers {
|
||||
if !producers[producer] {
|
||||
missing = append(missing, "event-producer."+producer)
|
||||
}
|
||||
}
|
||||
availability := domain.GameClientBridgeFeatureAvailability{Key: feature.Key, Available: len(missing) == 0}
|
||||
if len(missing) > 0 {
|
||||
availability.Reason = "compatible Companion is missing " + strings.Join(missing, ", ")
|
||||
}
|
||||
result = append(result, availability)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (svc *CoreService) ListGameClientBridgeCommandsForSession(sessionID string, filter domain.GameClientBridgeCommandFilter) ([]domain.GameClientBridgeCommand, error) {
|
||||
if err := svc.authorizeServerLifecycle(sessionID, filter.ServerInstanceID); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -361,7 +361,7 @@ func ValidatePluginCreateInputs(fields []domain.PluginCreateField, inputs map[st
|
||||
|
||||
func validateGameClientBridgeManifest(field string, bridge domain.GameClientBridgeManifest, permissions []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.Pages) == 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.Pages) == 0 && len(bridge.Features) == 0 && bridge.Retention.KeepForSeconds == 0 && bridge.Retention.MaxRecords == 0 && !companionPresent {
|
||||
return nil
|
||||
}
|
||||
var violations []string
|
||||
@@ -515,6 +515,50 @@ func validateGameClientBridgeManifest(field string, bridge domain.GameClientBrid
|
||||
for _, page := range pages {
|
||||
pageDeclarations[page.Key] = page
|
||||
}
|
||||
features := map[string]domain.GameClientBridgeFeatureDeclaration{}
|
||||
for index, feature := range bridge.Features {
|
||||
prefix := fmt.Sprintf("%s.features[%d]", field, index)
|
||||
if !clientManagerIdentifierPattern.MatchString(feature.Key) || unsafeGameClientBridgeCommandType(feature.Key) {
|
||||
violations = append(violations, prefix+".key is invalid or unsafe")
|
||||
}
|
||||
if _, exists := features[feature.Key]; exists {
|
||||
violations = append(violations, prefix+".key is duplicated")
|
||||
}
|
||||
features[feature.Key] = feature
|
||||
if strings.TrimSpace(feature.Title) == "" || len([]rune(feature.Title)) > 80 {
|
||||
violations = append(violations, prefix+".title is invalid")
|
||||
}
|
||||
if !containsString(permissions, feature.Permission) {
|
||||
violations = append(violations, prefix+".permission must be declared by the plugin")
|
||||
}
|
||||
if len(feature.RequiredHandlers) == 0 && len(feature.RequiredEventProducers) == 0 {
|
||||
violations = append(violations, prefix+" must require a handler or event producer")
|
||||
}
|
||||
for _, handler := range feature.RequiredHandlers {
|
||||
if !clientManagerIdentifierPattern.MatchString(handler) {
|
||||
violations = append(violations, prefix+".requiredHandlers contains an invalid handler")
|
||||
}
|
||||
}
|
||||
for _, producer := range feature.RequiredEventProducers {
|
||||
if !clientManagerIdentifierPattern.MatchString(producer) {
|
||||
violations = append(violations, prefix+".requiredEventProducers contains an invalid producer")
|
||||
}
|
||||
}
|
||||
violations = append(violations, duplicateViolations(prefix+".requiredHandlers", feature.RequiredHandlers)...)
|
||||
violations = append(violations, duplicateViolations(prefix+".requiredEventProducers", feature.RequiredEventProducers)...)
|
||||
}
|
||||
for _, page := range pages {
|
||||
for _, featureKey := range page.FeatureKeys {
|
||||
feature, exists := features[featureKey]
|
||||
if !exists {
|
||||
violations = append(violations, field+" page "+page.Key+" references undeclared feature "+featureKey)
|
||||
continue
|
||||
}
|
||||
if !containsString(page.Permissions, feature.Permission) {
|
||||
violations = append(violations, field+" page "+page.Key+" must declare feature permission "+feature.Permission)
|
||||
}
|
||||
}
|
||||
}
|
||||
for index, page := range bridge.Pages {
|
||||
prefix := fmt.Sprintf("%s.pages[%d]", field, index)
|
||||
pageDeclaration, pageExists := pageDeclarations[page.PageKey]
|
||||
@@ -551,6 +595,16 @@ func validateGameClientBridgeManifest(field string, bridge domain.GameClientBrid
|
||||
violations = append(violations, prefix+" must declare remote.access.request for query templates")
|
||||
}
|
||||
}
|
||||
for _, featureKey := range page.FeatureKeys {
|
||||
feature, exists := features[featureKey]
|
||||
if !exists {
|
||||
violations = append(violations, prefix+" references undeclared feature "+featureKey)
|
||||
continue
|
||||
}
|
||||
if !containsString(pageDeclaration.Permissions, feature.Permission) {
|
||||
violations = append(violations, prefix+" must declare feature permission "+feature.Permission)
|
||||
}
|
||||
}
|
||||
}
|
||||
return violations
|
||||
}
|
||||
@@ -1401,7 +1455,9 @@ func validatePluginPages(pages []domain.GamePluginPage) []string {
|
||||
violations = appendRequired(violations, prefix+".bundle.key", page.Bundle.Key)
|
||||
violations = appendRequired(violations, prefix+".bundle.version", page.Bundle.Version)
|
||||
violations = appendRequired(violations, prefix+".bundle.integritySha256", page.Bundle.IntegritySHA256)
|
||||
if !validDistributionLogicalKey(page.Bundle.Key) || !validPluginPageBundleVersion(page.Bundle.Version) || !validPluginPageBundleIntegrity(page.Bundle.IntegritySHA256) { violations = append(violations, prefix+".bundle is invalid") }
|
||||
if !validDistributionLogicalKey(page.Bundle.Key) || !validPluginPageBundleVersion(page.Bundle.Version) || !validPluginPageBundleIntegrity(page.Bundle.IntegritySHA256) {
|
||||
violations = append(violations, prefix+".bundle is invalid")
|
||||
}
|
||||
}
|
||||
if page.Key != "" {
|
||||
if _, exists := seenKeys[page.Key]; exists {
|
||||
@@ -1416,19 +1472,37 @@ func validatePluginPages(pages []domain.GamePluginPage) []string {
|
||||
}
|
||||
violations = append(violations, duplicateViolations(prefix+".permissions", page.Permissions)...)
|
||||
violations = append(violations, validateBridgeActions(prefix+".bridgeActions", page.BridgeActions)...)
|
||||
for _, key := range page.FeatureKeys {
|
||||
if !clientManagerIdentifierPattern.MatchString(key) {
|
||||
violations = append(violations, prefix+".featureKeys contains an invalid feature key")
|
||||
}
|
||||
}
|
||||
violations = append(violations, duplicateViolations(prefix+".featureKeys", page.FeatureKeys)...)
|
||||
}
|
||||
return violations
|
||||
}
|
||||
|
||||
func validPluginPageBundleVersion(value string) bool {
|
||||
if len(value) == 0 || len(value) > 80 { return false }
|
||||
for _, item := range value { if !(item >= 'a' && item <= 'z' || item >= 'A' && item <= 'Z' || item >= '0' && item <= '9' || item == '.' || item == '_' || item == '-') { return false } }
|
||||
if len(value) == 0 || len(value) > 80 {
|
||||
return false
|
||||
}
|
||||
for _, item := range value {
|
||||
if !(item >= 'a' && item <= 'z' || item >= 'A' && item <= 'Z' || item >= '0' && item <= '9' || item == '.' || item == '_' || item == '-') {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func validPluginPageBundleIntegrity(value string) bool {
|
||||
if len(value) != len("sha256:")+64 || !strings.HasPrefix(value, "sha256:") { return false }
|
||||
for _, item := range value[len("sha256:"):] { if !(item >= 'a' && item <= 'f' || item >= '0' && item <= '9') { return false } }
|
||||
if len(value) != len("sha256:")+64 || !strings.HasPrefix(value, "sha256:") {
|
||||
return false
|
||||
}
|
||||
for _, item := range value[len("sha256:"):] {
|
||||
if !(item >= 'a' && item <= 'f' || item >= '0' && item <= '9') {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -60,8 +60,11 @@ export interface GameClientBridgePageContractResponse {
|
||||
commandTypes?: string[];
|
||||
snapshotTypes?: string[];
|
||||
queryTemplateKeys?: string[];
|
||||
featureKeys?: string[];
|
||||
}
|
||||
|
||||
export interface GameClientBridgeFeatureDeclarationResponse { key: string; title: string; permission: string; requiredHandlers?: string[]; requiredEventProducers?: string[]; }
|
||||
|
||||
export interface GameClientBridgeCompanionDeclarationResponse {
|
||||
profileKey: string;
|
||||
configTemplateKey: string;
|
||||
@@ -85,6 +88,7 @@ export interface GameClientBridgeManifestResponse {
|
||||
commandRetentionSeconds: number;
|
||||
maxCommands: number;
|
||||
pages?: GameClientBridgePageContractResponse[];
|
||||
features?: GameClientBridgeFeatureDeclarationResponse[];
|
||||
companion?: GameClientBridgeCompanionDeclarationResponse;
|
||||
}
|
||||
|
||||
@@ -96,14 +100,19 @@ export interface GameClientBridgeProfileDeclarationResponse {
|
||||
commandTypes: string[];
|
||||
snapshotTypes: string[];
|
||||
queryTemplateKeys: string[];
|
||||
handlerTypes?: string[];
|
||||
eventProducerTypes?: string[];
|
||||
}
|
||||
|
||||
export interface GameClientBridgeFeatureAvailabilityResponse { key: string; available: boolean; reason?: string; }
|
||||
|
||||
export interface GameClientBridgeStatusResponse {
|
||||
serverInstanceId: string;
|
||||
pluginId: string;
|
||||
available: boolean;
|
||||
reason?: string;
|
||||
profiles: GameClientBridgeProfileDeclarationResponse[];
|
||||
features?: GameClientBridgeFeatureAvailabilityResponse[];
|
||||
}
|
||||
|
||||
export interface GameClientBridgeCommandResultResponse {
|
||||
@@ -224,6 +233,7 @@ export interface GamePluginPageResponse {
|
||||
bundleIntegritySha256?: string;
|
||||
permissions: string[];
|
||||
bridgeActions?: string[];
|
||||
featureKeys?: string[];
|
||||
}
|
||||
|
||||
export interface RuntimeDiscoveryProbeResponse {
|
||||
|
||||
@@ -83,6 +83,7 @@ export interface PluginPageContract {
|
||||
bundleIntegritySha256?: string;
|
||||
permissions?: PluginPermission[];
|
||||
bridgeActions?: PluginBridgeAction[];
|
||||
featureKeys?: string[];
|
||||
}
|
||||
|
||||
export interface PluginBridgeManifestContract {
|
||||
@@ -108,7 +109,8 @@ export function pluginBridgeManifestContractFromResponse(
|
||||
bundleVersion: page.bundleVersion,
|
||||
bundleIntegritySha256: page.bundleIntegritySha256,
|
||||
permissions: page.permissions.filter(isPluginPermission),
|
||||
bridgeActions: page.bridgeActions?.filter(isPluginBridgeAction)
|
||||
bridgeActions: page.bridgeActions?.filter(isPluginBridgeAction),
|
||||
featureKeys: page.featureKeys ? [...page.featureKeys] : []
|
||||
})),
|
||||
aiPurposes: [...plugin.aiPurposes]
|
||||
};
|
||||
|
||||
@@ -60,7 +60,11 @@ export function PluginPageHostPage({ params, onNavigate, initialPlugin }: Plugin
|
||||
setBundle(null); setBundleError("");
|
||||
void loadPluginPageBundle(declaredBundlePage).then((loaded) => { if (active) setBundle(() => loaded); }).catch((error) => { if (active) setBundleError(error instanceof Error ? error.message : "插件页面 bundle 加载失败。"); });
|
||||
if (!serverId) { setAvailability({ available: false, reason: "插件页面没有绑定服务器。" }); return () => { active = false; }; }
|
||||
void platformApiClient.getGameClientBridgeStatus(serverId).then((status) => { if (active) setAvailability({ available: status.available, reason: status.reason }); }).catch((error) => { if (active) setAvailability({ available: false, reason: error instanceof Error ? error.message : "无法验证 Companion 可用性。" }); });
|
||||
void platformApiClient.getGameClientBridgeStatus(serverId).then((status) => {
|
||||
const requiredFeatures = declaredBundlePage.featureKeys ?? [];
|
||||
const unavailable = requiredFeatures.map((key) => status.features?.find((feature) => feature.key === key)).find((feature) => !feature?.available);
|
||||
if (active) setAvailability(unavailable ? { available: false, reason: unavailable.reason || `功能 ${unavailable.key} 没有兼容的 Companion 实现。` } : { available: status.available, reason: status.reason });
|
||||
}).catch((error) => { if (active) setAvailability({ available: false, reason: error instanceof Error ? error.message : "无法验证 Companion 可用性。" }); });
|
||||
return () => { active = false; };
|
||||
}, [declaredBundlePage, serverId]);
|
||||
|
||||
|
||||
@@ -69,7 +69,8 @@ export function parseSafeGameClientBridgeStatus(value: unknown): GameClientBridg
|
||||
pluginId: string(record.pluginId, "pluginId"),
|
||||
available: boolean(record.available, "available"),
|
||||
reason: optionalString(record.reason, "reason"),
|
||||
profiles: array(record.profiles, "profiles").map(parseProfile)
|
||||
profiles: array(record.profiles, "profiles").map(parseProfile),
|
||||
features: array(record.features ?? [], "features").map(parseFeature)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -136,10 +137,17 @@ function parseProfile(value: unknown): GameClientBridgeProfileDeclarationRespons
|
||||
reason: optionalString(record.reason, "profile.reason"),
|
||||
commandTypes: stringArray(record.commandTypes, "profile.commandTypes"),
|
||||
snapshotTypes: stringArray(record.snapshotTypes, "profile.snapshotTypes"),
|
||||
queryTemplateKeys: stringArray(record.queryTemplateKeys, "profile.queryTemplateKeys")
|
||||
queryTemplateKeys: stringArray(record.queryTemplateKeys, "profile.queryTemplateKeys"),
|
||||
handlerTypes: stringArray(record.handlerTypes ?? [], "profile.handlerTypes"),
|
||||
eventProducerTypes: stringArray(record.eventProducerTypes ?? [], "profile.eventProducerTypes")
|
||||
};
|
||||
}
|
||||
|
||||
function parseFeature(value: unknown): { key: string; available: boolean; reason?: string } {
|
||||
const record = safeObject(value, "Game Client Bridge feature");
|
||||
return { key: string(record.key, "feature.key"), available: boolean(record.available, "feature.available"), reason: optionalString(record.reason, "feature.reason") };
|
||||
}
|
||||
|
||||
function parseResult(value: unknown): GameClientBridgeCommandResultResponse {
|
||||
const record = safeObject(value, "Game Client Bridge command result");
|
||||
const result: GameClientBridgeCommandResultResponse = {
|
||||
|
||||
@@ -242,6 +242,13 @@
|
||||
],
|
||||
"commandRetentionSeconds": 604800,
|
||||
"maxCommands": 1000,
|
||||
"features": [
|
||||
{ "key": "config.manage", "title": "SCUM configuration", "permission": "server.game-client.read", "requiredHandlers": ["config.read", "config.patch"] },
|
||||
{ "key": "player.intelligence", "title": "SCUM player intelligence", "permission": "server.game-client.read", "requiredHandlers": ["player.lookup"], "requiredEventProducers": ["semantic.events"] },
|
||||
{ "key": "reward.delivery", "title": "SCUM reward delivery", "permission": "server.game-client.command", "requiredHandlers": ["reward.deliver", "player.notify"] },
|
||||
{ "key": "state.patch", "title": "SCUM player state patch", "permission": "server.game-client.maintenance", "requiredHandlers": ["game-state.patch"] },
|
||||
{ "key": "trajectory.collect", "title": "SCUM trajectories", "permission": "server.game-client.read", "requiredEventProducers": ["semantic.events"] }
|
||||
],
|
||||
"pages": [
|
||||
{
|
||||
"pageKey": "files-config",
|
||||
@@ -254,7 +261,8 @@
|
||||
"restart.prepare",
|
||||
"maintenance.prepare"
|
||||
],
|
||||
"snapshotTypes": ["companion.health", "online.sessions", "players", "squads", "vehicles", "flags"]
|
||||
"snapshotTypes": ["companion.health", "online.sessions", "players", "squads", "vehicles", "flags"],
|
||||
"featureKeys": ["config.manage", "player.intelligence", "reward.delivery", "state.patch", "trajectory.collect"]
|
||||
}
|
||||
],
|
||||
"companion": {
|
||||
@@ -303,7 +311,7 @@
|
||||
"dependencyPolicy": "required",
|
||||
"approvalRequired": ["disable", "rollback", "retire"]
|
||||
},
|
||||
"pages": [{ "key": "files-config", "title": "文件、配置与玩家档案", "path": "/files-config", "bundleKey": "scum-server-plugin", "bundleVersion": "1.0.0", "bundleIntegritySha256": "sha256:8a4216107e1d7773d42a7e13b6466fd4fcf6e6bb2dc5f5af398fb7f4ea4f623b", "permissions": ["server.read", "server.files.read", "server.files.write", "server.logs.read", "server.game-client.read", "ai.invoke"], "bridgeActions": ["server.instances.read", "files.request", "logs.query", "ai.invoke"] }],
|
||||
"pages": [{ "key": "files-config", "title": "文件、配置与玩家档案", "path": "/files-config", "bundleKey": "scum-server-plugin", "bundleVersion": "1.0.0", "bundleIntegritySha256": "sha256:8a4216107e1d7773d42a7e13b6466fd4fcf6e6bb2dc5f5af398fb7f4ea4f623b", "permissions": ["server.read", "server.files.read", "server.files.write", "server.logs.read", "server.game-client.read", "server.game-client.command", "server.game-client.maintenance", "ai.invoke"], "bridgeActions": ["server.instances.read", "files.request", "logs.query", "ai.invoke"], "featureKeys": ["config.manage", "player.intelligence", "reward.delivery", "state.patch", "trajectory.collect"] }],
|
||||
"fileWorkspace": {
|
||||
"defaultDirectoryKey": "scum-config",
|
||||
"directories": [{ "key": "scum-config", "label": "服务器配置", "scope": "config" }, { "key": "scum-logs", "label": "日志文件", "scope": "logs" }],
|
||||
|
||||
@@ -190,7 +190,8 @@
|
||||
"bundleVersion": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$" },
|
||||
"bundleIntegritySha256": { "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" },
|
||||
"permissions": { "type": "array", "items": { "$ref": "#/$defs/pluginPermission" }, "uniqueItems": true },
|
||||
"bridgeActions": { "type": "array", "items": { "$ref": "#/$defs/bridgeAction" }, "uniqueItems": true }
|
||||
"bridgeActions": { "type": "array", "items": { "$ref": "#/$defs/bridgeAction" }, "uniqueItems": true },
|
||||
"featureKeys": { "type": "array", "items": { "type": "string" }, "uniqueItems": true }
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -262,6 +263,7 @@
|
||||
"items": { "$ref": "#/$defs/gameClientBridgePageContract" },
|
||||
"maxItems": 64
|
||||
},
|
||||
"features": { "type": "array", "items": { "$ref": "#/$defs/gameClientBridgeFeature" }, "maxItems": 128 },
|
||||
"companion": { "$ref": "#/$defs/gameClientBridgeCompanion" }
|
||||
}
|
||||
},
|
||||
@@ -337,7 +339,15 @@
|
||||
"pageKey": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]*$" },
|
||||
"commandTypes": { "type": "array", "items": { "type": "string" }, "uniqueItems": true },
|
||||
"snapshotTypes": { "type": "array", "items": { "type": "string" }, "uniqueItems": true },
|
||||
"queryTemplateKeys": { "type": "array", "items": { "type": "string" }, "uniqueItems": true }
|
||||
"queryTemplateKeys": { "type": "array", "items": { "type": "string" }, "uniqueItems": true },
|
||||
"featureKeys": { "type": "array", "items": { "type": "string" }, "uniqueItems": true }
|
||||
}
|
||||
},
|
||||
"gameClientBridgeFeature": {
|
||||
"type": "object", "required": ["key", "title", "permission"], "additionalProperties": false,
|
||||
"properties": {
|
||||
"key": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,159}$" }, "title": { "type": "string", "minLength": 1, "maxLength": 80 }, "permission": { "$ref": "#/$defs/pluginPermission" },
|
||||
"requiredHandlers": { "type": "array", "items": { "type": "string" }, "uniqueItems": true, "maxItems": 64 }, "requiredEventProducers": { "type": "array", "items": { "type": "string" }, "uniqueItems": true, "maxItems": 64 }
|
||||
}
|
||||
},
|
||||
"productionLifecycleOperation": {
|
||||
|
||||
@@ -250,8 +250,11 @@ export interface GameClientBridgePageContract {
|
||||
commandTypes?: string[];
|
||||
snapshotTypes?: string[];
|
||||
queryTemplateKeys?: string[];
|
||||
featureKeys?: string[];
|
||||
}
|
||||
|
||||
export interface GameClientBridgeFeatureDeclaration { key: string; title: string; permission: PluginPermission; requiredHandlers?: string[]; requiredEventProducers?: string[]; }
|
||||
|
||||
export interface GameClientBridgeCompanionDeclaration {
|
||||
profileKey: string;
|
||||
configTemplateKey: string;
|
||||
@@ -275,6 +278,7 @@ export interface GameClientBridgeManifest {
|
||||
commandRetentionSeconds: number;
|
||||
maxCommands: number;
|
||||
pages?: GameClientBridgePageContract[];
|
||||
features?: GameClientBridgeFeatureDeclaration[];
|
||||
companion?: GameClientBridgeCompanionDeclaration;
|
||||
}
|
||||
|
||||
@@ -286,14 +290,19 @@ export interface GameClientBridgeProfileStatus {
|
||||
commandTypes: string[];
|
||||
snapshotTypes: string[];
|
||||
queryTemplateKeys: string[];
|
||||
handlerTypes?: string[];
|
||||
eventProducerTypes?: string[];
|
||||
}
|
||||
|
||||
export interface GameClientBridgeFeatureAvailability { key: string; available: boolean; reason?: string; }
|
||||
|
||||
export interface GameClientBridgeStatus {
|
||||
serverInstanceId: string;
|
||||
pluginId: string;
|
||||
available: boolean;
|
||||
reason?: string;
|
||||
profiles: GameClientBridgeProfileStatus[];
|
||||
features?: GameClientBridgeFeatureAvailability[];
|
||||
}
|
||||
|
||||
export interface GameClientBridgeCommandResult {
|
||||
|
||||
Reference in New Issue
Block a user