feat(scum): add bounded vehicle spawn adapter

This commit is contained in:
npc0-hue
2026-07-29 16:04:26 +08:00
parent 7ae4dbf0b2
commit daa0f330a4
24 changed files with 314 additions and 23 deletions
@@ -17,6 +17,13 @@ that contract only for this exact source revision and UE4SS 3.0.1, with fixed
chat type `4`; it cannot accept arbitrary RCON text. Its generated command
text is private transport/audit data and never appears in a command result.
The same pinned `ScumBridge::trim_command` implementation removes at most one
leading `#` before dispatch. The authorized `vehicle.spawn` adapter preserves
the required `#spawnvehicle <vehicleCode>` template internally, supplies it
only to a Companion-local typed transport port, and never treats the source's
raw response as a stable acknowledgement. Its isolated port fixture supplies
the bounded success/failure/unknown receipt used by the adapter tests.
## Explicitly unavailable
The reference contains no versioned server-side producer or documented API for:
@@ -27,6 +34,11 @@ The reference contains no versioned server-side producer or documented API for:
- item/reward delivery; or
- skill/attribute read, safe-window checking, or mutation.
The fixed vehicle-spawn exception does not change these unavailable
capabilities and does not authorize arbitrary RCON commands, arguments,
targets, credentials, direct sockets, SQL, shell execution, or response
projection.
Therefore the Companion must not parse invented `LOGIN`/`LOGOUT` lines, upload
semantic events, correlate network identifiers, or claim trajectory, reward,
or game-state-patch support from this reference. The missing contract is a
@@ -42,6 +42,28 @@ type ue4SSPlayerNotification struct {
protectedAuditCommand string
}
// UE4SSVehicleSpawnPort is a Companion-local, platform-authorized transport
// for one fixed template. It accepts no raw command text, socket, credential,
// or host path. Implementations remain in this Companion package so the
// protected audit template cannot cross a general transport boundary.
type UE4SSVehicleSpawnPort interface {
SpawnVehicle(context.Context, ue4SSVehicleSpawn) (UE4SSVehicleSpawnReceipt, error)
}
type UE4SSVehicleSpawnOutcome string
const (
UE4SSVehicleSpawnAccepted UE4SSVehicleSpawnOutcome = "accepted"
UE4SSVehicleSpawnRejected UE4SSVehicleSpawnOutcome = "rejected"
UE4SSVehicleSpawnUnknown UE4SSVehicleSpawnOutcome = "unknown"
)
type UE4SSVehicleSpawnReceipt struct{ Outcome UE4SSVehicleSpawnOutcome }
type ue4SSVehicleSpawn struct {
ServerID string
VehicleCode string
protectedAuditCommand string
}
type VersionedAdapter struct {
BoundServerID string
ServerVersion string
@@ -49,6 +71,7 @@ type VersionedAdapter struct {
UE4SSReferenceRevision string
Config AuthorizedConfigPort
Notification UE4SSNotificationPort
VehicleSpawn UE4SSVehicleSpawnPort
DiagnosticsState map[string]string
}
@@ -103,7 +126,7 @@ func (VersionedAdapter) DeliverReward(context.Context, map[string]any) (map[stri
return nil, fmt.Errorf("reward adapter is unsupported")
}
func (adapter VersionedAdapter) NotifyPlayer(ctx context.Context, payload map[string]any) (map[string]any, error) {
if !adapter.supportsUE4SSNotification() || adapter.Notification == nil {
if !adapter.supportsPinnedUE4SS() || adapter.Notification == nil {
return nil, fmt.Errorf("notification adapter is unsupported")
}
playerID, playerOK := payload["playerId"].(string)
@@ -121,12 +144,33 @@ func (adapter VersionedAdapter) NotifyPlayer(ctx context.Context, payload map[st
}
return map[string]any{"accepted": true, "message": "notification accepted for online recipient"}, nil
}
func (adapter VersionedAdapter) SpawnVehicle(ctx context.Context, payload map[string]any) (map[string]any, error) {
if !adapter.supportsPinnedUE4SS() || adapter.VehicleSpawn == nil {
return nil, fmt.Errorf("vehicle spawn adapter is unsupported")
}
vehicleCode, ok := payload["vehicleCode"].(string)
spawn, err := newUE4SSVehicleSpawn(adapter.BoundServerID, vehicleCode)
if !ok || err != nil {
return nil, fmt.Errorf("vehicle spawn payload is invalid")
}
receipt, err := adapter.VehicleSpawn.SpawnVehicle(ctx, spawn)
if err != nil || receipt.Outcome == UE4SSVehicleSpawnUnknown {
return map[string]any{"outcome": "unknown"}, nil
}
if receipt.Outcome == UE4SSVehicleSpawnRejected {
return map[string]any{"outcome": "failed"}, nil
}
if receipt.Outcome != UE4SSVehicleSpawnAccepted {
return map[string]any{"outcome": "unknown"}, nil
}
return map[string]any{"outcome": "succeeded"}, nil
}
func (adapter VersionedAdapter) supportsUE4SSNotification() bool {
func (adapter VersionedAdapter) supportsPinnedUE4SS() bool {
// The pinned source reflects SendChatLineToPlayer at runtime and fails
// closed on a schema change, so no unverified SCUM-version mapping is
// embedded here. The dispatcher still requires a discovered server version.
return adapter.BoundServerID != "" && adapter.UE4SSBuild == UE4SSReferenceBuild && adapter.UE4SSReferenceRevision == UE4SSReferenceRevision
return adapter.BoundServerID != "" && supportedAdapterVersion(adapter.ServerVersion) && adapter.UE4SSBuild == UE4SSReferenceBuild && adapter.UE4SSReferenceRevision == UE4SSReferenceRevision
}
func newUE4SSPlayerNotification(serverID, playerID, message string) (ue4SSPlayerNotification, error) {
if strings.TrimSpace(serverID) == "" || !steamID64(playerID) || !validNotificationMessage(message) {
@@ -134,6 +178,12 @@ func newUE4SSPlayerNotification(serverID, playerID, message string) (ue4SSPlayer
}
return ue4SSPlayerNotification{ServerID: serverID, RecipientSteamID: playerID, Message: message, chatType: fixedNotificationType, protectedAuditCommand: "SendChat 4 \"" + escapeUE4SSChatMessage(message) + "\" " + playerID}, nil
}
func newUE4SSVehicleSpawn(serverID, vehicleCode string) (ue4SSVehicleSpawn, error) {
if strings.TrimSpace(serverID) == "" || !supportedVehicleSpawnCode(vehicleCode) {
return ue4SSVehicleSpawn{}, fmt.Errorf("invalid typed UE4SS vehicle spawn")
}
return ue4SSVehicleSpawn{ServerID: serverID, VehicleCode: vehicleCode, protectedAuditCommand: "#spawnvehicle " + vehicleCode}, nil
}
func steamID64(value string) bool {
if len(value) != 17 {
return false
@@ -161,6 +211,9 @@ func escapeUE4SSChatMessage(value string) string {
}
func supportedAdapterVersion(version string) bool { return version == "0.9.700.90357" }
func supportedVehicleSpawnCode(value string) bool {
return map[string]bool{"BPC_Laika_C": true, "BPC_WolfsWagen_C": true}[value]
}
func supportedConfigKey(key string) bool {
return map[string]bool{"ServerName": true, "GamePort": true, "QueryPort": true, "MaxPlayers": true, "WelcomeMessage": true}[key]
}
@@ -2,6 +2,7 @@ package companion
import (
"context"
"errors"
"testing"
"time"
)
@@ -24,6 +25,20 @@ type notificationPortFixture struct {
accepted bool
}
// nonProductionVehicleSpawnPortFixture is an isolated test double. It has no
// network, socket, credential, or raw-command entry point; it can observe only
// the Companion's private typed request and return a bounded receipt.
type nonProductionVehicleSpawnPortFixture struct {
requests []ue4SSVehicleSpawn
receipt UE4SSVehicleSpawnReceipt
err error
}
func (fixture *nonProductionVehicleSpawnPortFixture) SpawnVehicle(_ context.Context, request ue4SSVehicleSpawn) (UE4SSVehicleSpawnReceipt, error) {
fixture.requests = append(fixture.requests, request)
return fixture.receipt, fixture.err
}
func (fixture *notificationPortFixture) SendPlayerNotification(_ context.Context, notification ue4SSPlayerNotification) (UE4SSNotificationReceipt, error) {
fixture.deliveries = append(fixture.deliveries, notification)
return UE4SSNotificationReceipt{Accepted: fixture.accepted}, nil
@@ -97,3 +112,64 @@ func TestNotificationFailureIsCachedWithoutInvokingRewardDelivery(t *testing.T)
t.Fatalf("duplicate notification attempted transport %d times", len(port.deliveries))
}
}
func TestVersionedVehicleSpawnUsesFixedTemplateAndPrivateAuditOnly(t *testing.T) {
port := &nonProductionVehicleSpawnPortFixture{receipt: UE4SSVehicleSpawnReceipt{Outcome: UE4SSVehicleSpawnAccepted}}
adapter := VersionedAdapter{BoundServerID: "server-1", ServerVersion: "0.9.700.90357", UE4SSBuild: UE4SSReferenceBuild, UE4SSReferenceRevision: UE4SSReferenceRevision, VehicleSpawn: port}
result, err := adapter.SpawnVehicle(context.Background(), map[string]any{"vehicleCode": "BPC_Laika_C"})
if err != nil || result["outcome"] != "succeeded" || len(port.requests) != 1 {
t.Fatalf("fixed vehicle spawn was not delivered: result=%+v err=%v requests=%+v", result, err, port.requests)
}
request := port.requests[0]
if request.ServerID != "server-1" || request.VehicleCode != "BPC_Laika_C" || request.protectedAuditCommand != "#spawnvehicle BPC_Laika_C" {
t.Fatalf("vehicle spawn did not use the fixed template: %+v", request)
}
if result["command"] != nil || result["rcon"] != nil || result["audit"] != nil || result["outcome"] == request.protectedAuditCommand {
t.Fatalf("vehicle spawn leaked protected transport details: %+v", result)
}
}
func TestVersionedVehicleSpawnFailsClosedAndClassifiesBoundedReceipts(t *testing.T) {
port := &nonProductionVehicleSpawnPortFixture{receipt: UE4SSVehicleSpawnReceipt{Outcome: UE4SSVehicleSpawnRejected}}
adapter := VersionedAdapter{BoundServerID: "server-1", ServerVersion: "0.9.700.90357", UE4SSBuild: UE4SSReferenceBuild, UE4SSReferenceRevision: UE4SSReferenceRevision, VehicleSpawn: port}
for name, testCase := range map[string]struct {
outcome string
receipt UE4SSVehicleSpawnReceipt
err error
}{
"failed": {outcome: "failed", receipt: UE4SSVehicleSpawnReceipt{Outcome: UE4SSVehicleSpawnRejected}},
"unknown": {outcome: "unknown", receipt: UE4SSVehicleSpawnReceipt{Outcome: UE4SSVehicleSpawnUnknown}},
"unknown-transport": {outcome: "unknown", receipt: UE4SSVehicleSpawnReceipt{}, err: errors.New("isolated transport timeout")},
} {
port.receipt, port.err = testCase.receipt, testCase.err
result, err := adapter.SpawnVehicle(context.Background(), map[string]any{"vehicleCode": "BPC_WolfsWagen_C"})
if err != nil || result["outcome"] != testCase.outcome {
t.Fatalf("receipt %s was not safely classified: result=%+v err=%v", name, result, err)
}
}
before := len(port.requests)
if _, err := adapter.SpawnVehicle(context.Background(), map[string]any{"vehicleCode": "#spawnvehicle BPC_Laika_C"}); err == nil || len(port.requests) != before {
t.Fatal("raw command text must not reach the vehicle transport")
}
adapter.UE4SSBuild = "3.0.2"
if _, err := adapter.SpawnVehicle(context.Background(), map[string]any{"vehicleCode": "BPC_Laika_C"}); err == nil || len(port.requests) != before {
t.Fatal("unpinned UE4SS build must not reach the vehicle transport")
}
}
func TestVehicleSpawnUnknownOutcomeIsCachedWithoutRetry(t *testing.T) {
stamp := time.Now().UTC()
port := &nonProductionVehicleSpawnPortFixture{receipt: UE4SSVehicleSpawnReceipt{Outcome: UE4SSVehicleSpawnUnknown}}
adapter := VersionedAdapter{BoundServerID: "server-1", ServerVersion: "0.9.700.90357", UE4SSBuild: UE4SSReferenceBuild, UE4SSReferenceRevision: UE4SSReferenceRevision, VehicleSpawn: port}
registry := NewHandlerRegistry(HandlerAvailability{BoundServerID: "server-1", ServerVersion: "0.9.700.90357", Approved: true, Capabilities: map[string]bool{"vehicle.spawn": true}}, adapter)
command := ClaimedCommand{ID: "vehicle-unknown-1", ProfileKey: ProfileKey, CommandType: "vehicle.spawn", Payload: map[string]any{"vehicleCode": "BPC_Laika_C"}, FencingToken: 1, LeaseExpiresAt: stamp.Add(time.Minute), ExpiresAt: stamp.Add(time.Minute)}
for range 2 {
result, err := registry.Execute(context.Background(), command)
if err != nil || result.Payload["outcome"] != "unknown" {
t.Fatalf("unknown vehicle outcome was not retained: result=%+v err=%v", result, err)
}
}
if len(port.requests) != 1 {
t.Fatalf("unknown vehicle outcome retried transport %d times", len(port.requests))
}
}
@@ -25,6 +25,12 @@ var requiredCapabilities = []string{
"game-client.bridge",
"logs.stream",
}
var optionalCapabilities = map[string]struct{}{
"handler.vehicle.spawn": {},
}
var requiredCapabilitySet = map[string]struct{}{
"component.register": {}, "component.heartbeat": {}, "component.health": {}, "component.control": {}, "game-client.bridge": {}, "logs.stream": {},
}
type Config struct {
SchemaVersion int `json:"schemaVersion" yaml:"schemaVersion"`
@@ -147,7 +153,7 @@ func canonicalPlatformOrigin(value string) (string, error) {
}
func validateCapabilities(capabilities []string) error {
if len(capabilities) != len(requiredCapabilities) {
if len(capabilities) < len(requiredCapabilities) || len(capabilities) > len(requiredCapabilities)+len(optionalCapabilities) {
return fmt.Errorf("component capabilities do not match the SCUM companion profile")
}
actual := make(map[string]struct{}, len(capabilities))
@@ -162,5 +168,12 @@ func validateCapabilities(capabilities []string) error {
return fmt.Errorf("component capabilities do not match the SCUM companion profile")
}
}
for capability := range actual {
if _, required := requiredCapabilitySet[capability]; !required {
if _, optional := optionalCapabilities[capability]; !optional {
return fmt.Errorf("component capabilities do not match the SCUM companion profile")
}
}
}
return nil
}
@@ -0,0 +1,16 @@
package companion
import "testing"
func TestCompanionVehicleHandlerCapabilityIsExplicitAndBounded(t *testing.T) {
base := append([]string(nil), requiredCapabilities...)
if err := validateCapabilities(base); err != nil {
t.Fatalf("base companion profile must remain valid: %v", err)
}
if err := validateCapabilities(append(base, "handler.vehicle.spawn")); err != nil {
t.Fatalf("explicit vehicle handler declaration must be valid: %v", err)
}
if err := validateCapabilities(append(base, "handler.raw.rcon")); err == nil {
t.Fatal("undeclared raw command handler capability must be rejected")
}
}
@@ -17,6 +17,7 @@ type SafeAdapter interface {
PatchGameState(context.Context, map[string]any) (map[string]any, error)
DeliverReward(context.Context, map[string]any) (map[string]any, error)
NotifyPlayer(context.Context, map[string]any) (map[string]any, error)
SpawnVehicle(context.Context, map[string]any) (map[string]any, error)
}
type HandlerAvailability struct {
@@ -54,6 +55,9 @@ func NewHandlerRegistry(availability HandlerAvailability, adapter SafeAdapter) *
registry.handlers["player.notify"] = func(ctx context.Context, payload map[string]any) (map[string]any, error) {
return adapter.NotifyPlayer(ctx, payload)
}
registry.handlers["vehicle.spawn"] = func(ctx context.Context, payload map[string]any) (map[string]any, error) {
return adapter.SpawnVehicle(ctx, payload)
}
return registry
}
@@ -189,6 +193,18 @@ func validateCommandPayload(commandType string, payload map[string]any) error {
return fmt.Errorf("notification payload is invalid")
}
return nil
case "vehicle.spawn":
if err := require("vehicleCode"); err != nil {
return err
}
if err := noUnknown("vehicleCode"); err != nil {
return err
}
vehicleCode, vehicleOK := payload["vehicleCode"].(string)
if !vehicleOK || !supportedVehicleSpawnCode(vehicleCode) {
return fmt.Errorf("vehicle spawn payload is invalid")
}
return nil
default:
return fmt.Errorf("command type is not declared")
}
@@ -45,6 +45,9 @@ func (*adapterFixture) DeliverReward(context.Context, map[string]any) (map[strin
func (*adapterFixture) NotifyPlayer(context.Context, map[string]any) (map[string]any, error) {
return nil, nil
}
func (*adapterFixture) SpawnVehicle(context.Context, map[string]any) (map[string]any, error) {
return nil, nil
}
func TestDispatcherAcknowledgesOnlyLiveValidatedTypedCommands(t *testing.T) {
stamp := time.Now().UTC()
@@ -82,7 +85,7 @@ func TestRegistryReturnsCachedResultForDuplicateDelivery(t *testing.T) {
func TestRegistryRejectsUndeclaredAndMalformedPayloads(t *testing.T) {
stamp := time.Now().UTC()
registry := NewHandlerRegistry(HandlerAvailability{BoundServerID: "server-1", ServerVersion: "0.9.700.90357", Approved: true, Capabilities: map[string]bool{"config.patch": true}}, &adapterFixture{})
for _, command := range []ClaimedCommand{{ID: "bad-type", ProfileKey: ProfileKey, CommandType: "raw.rcon", Payload: map[string]any{}, FencingToken: 1, LeaseExpiresAt: stamp.Add(time.Minute), ExpiresAt: stamp.Add(time.Minute)}, {ID: "bad-payload", ProfileKey: ProfileKey, CommandType: "config.patch", Payload: map[string]any{"revision": "r1"}, FencingToken: 1, LeaseExpiresAt: stamp.Add(time.Minute), ExpiresAt: stamp.Add(time.Minute)}} {
for _, command := range []ClaimedCommand{{ID: "bad-type", ProfileKey: ProfileKey, CommandType: "raw.rcon", Payload: map[string]any{}, FencingToken: 1, LeaseExpiresAt: stamp.Add(time.Minute), ExpiresAt: stamp.Add(time.Minute)}, {ID: "bad-payload", ProfileKey: ProfileKey, CommandType: "config.patch", Payload: map[string]any{"revision": "r1"}, FencingToken: 1, LeaseExpiresAt: stamp.Add(time.Minute), ExpiresAt: stamp.Add(time.Minute)}, {ID: "unsafe-vehicle", ProfileKey: ProfileKey, CommandType: "vehicle.spawn", Payload: map[string]any{"vehicleCode": "BPC_Laika_C", "command": "#spawnvehicle BPC_Laika_C"}, FencingToken: 1, LeaseExpiresAt: stamp.Add(time.Minute), ExpiresAt: stamp.Add(time.Minute)}} {
result, err := registry.Execute(context.Background(), command)
if err != nil || result.Payload["result"] != "validation-failed" {
t.Fatalf("unsafe command was not rejected: result=%+v err=%v", result, err)