diff --git a/docs/input-capabilities.md b/docs/input-capabilities.md index a9c1f89..54064f3 100644 --- a/docs/input-capabilities.md +++ b/docs/input-capabilities.md @@ -20,3 +20,9 @@ release. The `axis.v1` model writes the current normalized axis value. Clients send changed values through the action's advertised operation; providers own interpretation, persistence, receipts, and simulation. A client must not infer decay or repair a missed release by mutating presented state. + +The `view-direction.v1` model is a one-shot value sampled when its gesture is +performed. The client reads the active semantic view's normalized world-space +forward direction and writes X, Y, and Z to the three advertised `payloadKeys` +in that order. The provider decides what the ray means; clients do not select, +filter, or rank gameplay targets. diff --git a/packages/eve-contracts/README.md b/packages/eve-contracts/README.md index 31cc69d..efa861d 100644 --- a/packages/eve-contracts/README.md +++ b/packages/eve-contracts/README.md @@ -10,11 +10,13 @@ import { parseEveProviderAdvertisement, parseEveSurfaceDocument, parseEveCommandInvocation, + parseEveInputCapability, } from "@gamecult/eve-contracts"; const provider = parseEveProviderAdvertisement(untrustedAdvertisement); const surface = parseEveSurfaceDocument(untrustedSurface); const command = parseEveCommandInvocation(untrustedCommand); +const input = parseEveInputCapability(untrustedInputCapability); ``` Providers own their state, surfaces, command acceptance, and receipts. This diff --git a/packages/eve-contracts/dist/generated/input-capability.d.ts b/packages/eve-contracts/dist/generated/input-capability.d.ts new file mode 100644 index 0000000..dbc35b2 --- /dev/null +++ b/packages/eve-contracts/dist/generated/input-capability.d.ts @@ -0,0 +1,51 @@ +export type InputValue = ({ + [k: string]: any; +} & { + model: ("button-hold.v1" | "axis.v1" | "view-direction.v1"); + payloadKey?: string; + /** + * @minItems 3 + * @maxItems 3 + */ + payloadKeys?: [string, string, string]; +}); +export interface EveInputCapability { + schema: "gamecult.eve.input_capability.v1"; + providerId: string; + capabilityId: string; + version?: number; + actions: { + actionId: string; + label: string; + operation: string; + context?: string; + category?: string; + iconRef?: string; + availability: ("available" | "dormant" | "unavailable"); + sourceRef?: string; + payload?: { + [k: string]: string; + }; + inputValue?: InputValue; + }[]; + defaultProfiles: { + profileId: string; + deviceClass: ("keyboard-mouse" | "gamepad"); + bindings: Binding[]; + }[]; +} +export interface Binding { + bindingId: string; + actionId: string; + gesture: Gesture; + actionBar?: boolean; +} +export interface Gesture { + kind: ("direct" | "chord" | "sequence" | "axis"); + /** + * @minItems 1 + */ + controls: [string, ...(string)[]]; + maxStepIntervalMs?: number; + completionControl?: string; +} diff --git a/packages/eve-contracts/dist/generated/input-capability.js b/packages/eve-contracts/dist/generated/input-capability.js new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/packages/eve-contracts/dist/generated/input-capability.js @@ -0,0 +1 @@ +export {}; diff --git a/packages/eve-contracts/dist/generated/schemas.d.ts b/packages/eve-contracts/dist/generated/schemas.d.ts index 4a35c30..af345cb 100644 --- a/packages/eve-contracts/dist/generated/schemas.d.ts +++ b/packages/eve-contracts/dist/generated/schemas.d.ts @@ -625,5 +625,186 @@ export declare const eveContractSchemas: { }; readonly additionalProperties: true; }; + readonly inputCapability: { + readonly $schema: "https://json-schema.org/draft/2020-12/schema"; + readonly $id: "gamecult.eve.input_capability.v1"; + readonly title: "Eve Input Capability"; + readonly type: "object"; + readonly required: readonly ["schema", "providerId", "capabilityId", "actions", "defaultProfiles"]; + readonly properties: { + readonly schema: { + readonly const: "gamecult.eve.input_capability.v1"; + }; + readonly providerId: { + readonly type: "string"; + readonly minLength: 1; + }; + readonly capabilityId: { + readonly type: "string"; + readonly minLength: 1; + }; + readonly version: { + readonly type: "integer"; + readonly minimum: 0; + }; + readonly actions: { + readonly type: "array"; + readonly items: { + readonly type: "object"; + readonly required: readonly ["actionId", "label", "operation", "availability"]; + readonly properties: { + readonly actionId: { + readonly type: "string"; + readonly minLength: 1; + }; + readonly label: { + readonly type: "string"; + readonly minLength: 1; + }; + readonly operation: { + readonly type: "string"; + readonly minLength: 1; + }; + readonly context: { + readonly type: "string"; + }; + readonly category: { + readonly type: "string"; + }; + readonly iconRef: { + readonly type: "string"; + }; + readonly availability: { + readonly enum: readonly ["available", "dormant", "unavailable"]; + }; + readonly sourceRef: { + readonly type: "string"; + }; + readonly payload: { + readonly type: "object"; + readonly additionalProperties: { + readonly type: "string"; + }; + }; + readonly inputValue: { + readonly $ref: "#/$defs/inputValue"; + }; + }; + readonly additionalProperties: false; + }; + }; + readonly defaultProfiles: { + readonly type: "array"; + readonly items: { + readonly type: "object"; + readonly required: readonly ["profileId", "deviceClass", "bindings"]; + readonly properties: { + readonly profileId: { + readonly type: "string"; + readonly minLength: 1; + }; + readonly deviceClass: { + readonly enum: readonly ["keyboard-mouse", "gamepad"]; + }; + readonly bindings: { + readonly type: "array"; + readonly items: { + readonly $ref: "#/$defs/binding"; + }; + }; + }; + readonly additionalProperties: false; + }; + }; + }; + readonly $defs: { + readonly inputValue: { + readonly type: "object"; + readonly required: readonly ["model"]; + readonly properties: { + readonly model: { + readonly enum: readonly ["button-hold.v1", "axis.v1", "view-direction.v1"]; + }; + readonly payloadKey: { + readonly type: "string"; + readonly minLength: 1; + }; + readonly payloadKeys: { + readonly type: "array"; + readonly minItems: 3; + readonly maxItems: 3; + readonly items: { + readonly type: "string"; + readonly minLength: 1; + }; + }; + }; + readonly allOf: readonly [{ + readonly if: { + readonly properties: { + readonly model: { + readonly const: "view-direction.v1"; + }; + }; + readonly required: readonly ["model"]; + }; + readonly then: { + readonly required: readonly ["payloadKeys"]; + }; + readonly else: { + readonly required: readonly ["payloadKey"]; + }; + }]; + readonly additionalProperties: false; + }; + readonly gesture: { + readonly type: "object"; + readonly required: readonly ["kind", "controls"]; + readonly properties: { + readonly kind: { + readonly enum: readonly ["direct", "chord", "sequence", "axis"]; + }; + readonly controls: { + readonly type: "array"; + readonly minItems: 1; + readonly items: { + readonly type: "string"; + readonly minLength: 1; + }; + }; + readonly maxStepIntervalMs: { + readonly type: "integer"; + readonly minimum: 50; + }; + readonly completionControl: { + readonly type: "string"; + }; + }; + readonly additionalProperties: false; + }; + readonly binding: { + readonly type: "object"; + readonly required: readonly ["bindingId", "actionId", "gesture"]; + readonly properties: { + readonly bindingId: { + readonly type: "string"; + readonly minLength: 1; + }; + readonly actionId: { + readonly type: "string"; + readonly minLength: 1; + }; + readonly gesture: { + readonly $ref: "#/$defs/gesture"; + }; + readonly actionBar: { + readonly type: "boolean"; + }; + }; + readonly additionalProperties: false; + }; + }; + readonly additionalProperties: false; + }; }; export type EveContractSchemaName = keyof typeof eveContractSchemas; diff --git a/packages/eve-contracts/dist/generated/schemas.js b/packages/eve-contracts/dist/generated/schemas.js index 58e7b43..99cdaa8 100644 --- a/packages/eve-contracts/dist/generated/schemas.js +++ b/packages/eve-contracts/dist/generated/schemas.js @@ -748,5 +748,234 @@ export const eveContractSchemas = { } }, "additionalProperties": true + }, + "inputCapability": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "gamecult.eve.input_capability.v1", + "title": "Eve Input Capability", + "type": "object", + "required": [ + "schema", + "providerId", + "capabilityId", + "actions", + "defaultProfiles" + ], + "properties": { + "schema": { + "const": "gamecult.eve.input_capability.v1" + }, + "providerId": { + "type": "string", + "minLength": 1 + }, + "capabilityId": { + "type": "string", + "minLength": 1 + }, + "version": { + "type": "integer", + "minimum": 0 + }, + "actions": { + "type": "array", + "items": { + "type": "object", + "required": [ + "actionId", + "label", + "operation", + "availability" + ], + "properties": { + "actionId": { + "type": "string", + "minLength": 1 + }, + "label": { + "type": "string", + "minLength": 1 + }, + "operation": { + "type": "string", + "minLength": 1 + }, + "context": { + "type": "string" + }, + "category": { + "type": "string" + }, + "iconRef": { + "type": "string" + }, + "availability": { + "enum": [ + "available", + "dormant", + "unavailable" + ] + }, + "sourceRef": { + "type": "string" + }, + "payload": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "inputValue": { + "$ref": "#/$defs/inputValue" + } + }, + "additionalProperties": false + } + }, + "defaultProfiles": { + "type": "array", + "items": { + "type": "object", + "required": [ + "profileId", + "deviceClass", + "bindings" + ], + "properties": { + "profileId": { + "type": "string", + "minLength": 1 + }, + "deviceClass": { + "enum": [ + "keyboard-mouse", + "gamepad" + ] + }, + "bindings": { + "type": "array", + "items": { + "$ref": "#/$defs/binding" + } + } + }, + "additionalProperties": false + } + } + }, + "$defs": { + "inputValue": { + "type": "object", + "required": [ + "model" + ], + "properties": { + "model": { + "enum": [ + "button-hold.v1", + "axis.v1", + "view-direction.v1" + ] + }, + "payloadKey": { + "type": "string", + "minLength": 1 + }, + "payloadKeys": { + "type": "array", + "minItems": 3, + "maxItems": 3, + "items": { + "type": "string", + "minLength": 1 + } + } + }, + "allOf": [ + { + "if": { + "properties": { + "model": { + "const": "view-direction.v1" + } + }, + "required": [ + "model" + ] + }, + "then": { + "required": [ + "payloadKeys" + ] + }, + "else": { + "required": [ + "payloadKey" + ] + } + } + ], + "additionalProperties": false + }, + "gesture": { + "type": "object", + "required": [ + "kind", + "controls" + ], + "properties": { + "kind": { + "enum": [ + "direct", + "chord", + "sequence", + "axis" + ] + }, + "controls": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "minLength": 1 + } + }, + "maxStepIntervalMs": { + "type": "integer", + "minimum": 50 + }, + "completionControl": { + "type": "string" + } + }, + "additionalProperties": false + }, + "binding": { + "type": "object", + "required": [ + "bindingId", + "actionId", + "gesture" + ], + "properties": { + "bindingId": { + "type": "string", + "minLength": 1 + }, + "actionId": { + "type": "string", + "minLength": 1 + }, + "gesture": { + "$ref": "#/$defs/gesture" + }, + "actionBar": { + "type": "boolean" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false } }; diff --git a/packages/eve-contracts/dist/index.d.ts b/packages/eve-contracts/dist/index.d.ts index f880110..d36f7e8 100644 --- a/packages/eve-contracts/dist/index.d.ts +++ b/packages/eve-contracts/dist/index.d.ts @@ -2,12 +2,14 @@ import { type ErrorObject } from "ajv/dist/2020.js"; import type { EveCommandDescriptor } from "./generated/command-descriptor.js"; import type { EveCommandInvocation } from "./generated/command-invocation.js"; import type { EveCommandReceipt } from "./generated/command-receipt.js"; +import type { EveInputCapability } from "./generated/input-capability.js"; import type { EveProviderAdvertisement } from "./generated/provider-advertisement.js"; import type { EveSurfaceDocument } from "./generated/surface.js"; import { type EveContractSchemaName } from "./generated/schemas.js"; export type { EveCommandDescriptor } from "./generated/command-descriptor.js"; export type { EveCommandInvocation } from "./generated/command-invocation.js"; export type { EveCommandReceipt } from "./generated/command-receipt.js"; +export type { EveInputCapability } from "./generated/input-capability.js"; export type { EveProviderAdvertisement } from "./generated/provider-advertisement.js"; export type { EveSurfaceDocument } from "./generated/surface.js"; export { eveContractSchemas, type EveContractSchemaName } from "./generated/schemas.js"; @@ -16,12 +18,14 @@ export declare const EVE_SURFACE_SCHEMA: "gamecult.eve.surface.v1"; export declare const EVE_COMMAND_DESCRIPTOR_SCHEMA: "gamecult.eve.command.v1"; export declare const EVE_COMMAND_INVOCATION_SCHEMA: "gamecult.eve.command_invocation.v1"; export declare const EVE_COMMAND_RECEIPT_SCHEMA: "gamecult.eve.command_receipt.v1"; +export declare const EVE_INPUT_CAPABILITY_SCHEMA: "gamecult.eve.input_capability.v1"; type ContractTypes = { providerAdvertisement: EveProviderAdvertisement; surface: EveSurfaceDocument; commandDescriptor: EveCommandDescriptor; commandInvocation: EveCommandInvocation; commandReceipt: EveCommandReceipt; + inputCapability: EveInputCapability; }; export declare class EveContractValidationError extends TypeError { readonly contract: EveContractSchemaName; @@ -35,8 +39,10 @@ export declare const isEveSurfaceDocument: (value: unknown) => value is EveSurfa export declare const isEveCommandDescriptor: (value: unknown) => value is EveCommandDescriptor; export declare const isEveCommandInvocation: (value: unknown) => value is EveCommandInvocation; export declare const isEveCommandReceipt: (value: unknown) => value is EveCommandReceipt; +export declare const isEveInputCapability: (value: unknown) => value is EveInputCapability; export declare const parseEveProviderAdvertisement: (value: unknown) => EveProviderAdvertisement; export declare const parseEveSurfaceDocument: (value: unknown) => EveSurfaceDocument; export declare const parseEveCommandDescriptor: (value: unknown) => EveCommandDescriptor; export declare const parseEveCommandInvocation: (value: unknown) => EveCommandInvocation; export declare const parseEveCommandReceipt: (value: unknown) => EveCommandReceipt; +export declare const parseEveInputCapability: (value: unknown) => EveInputCapability; diff --git a/packages/eve-contracts/dist/index.js b/packages/eve-contracts/dist/index.js index c409e63..9320db7 100644 --- a/packages/eve-contracts/dist/index.js +++ b/packages/eve-contracts/dist/index.js @@ -6,6 +6,7 @@ export const EVE_SURFACE_SCHEMA = "gamecult.eve.surface.v1"; export const EVE_COMMAND_DESCRIPTOR_SCHEMA = "gamecult.eve.command.v1"; export const EVE_COMMAND_INVOCATION_SCHEMA = "gamecult.eve.command_invocation.v1"; export const EVE_COMMAND_RECEIPT_SCHEMA = "gamecult.eve.command_receipt.v1"; +export const EVE_INPUT_CAPABILITY_SCHEMA = "gamecult.eve.input_capability.v1"; const ajv = new Ajv2020({ allErrors: true, allowUnionTypes: true, @@ -40,11 +41,13 @@ export const isEveSurfaceDocument = (value) => isEveContract("surface", value); export const isEveCommandDescriptor = (value) => isEveContract("commandDescriptor", value); export const isEveCommandInvocation = (value) => isEveContract("commandInvocation", value); export const isEveCommandReceipt = (value) => isEveContract("commandReceipt", value); +export const isEveInputCapability = (value) => isEveContract("inputCapability", value); export const parseEveProviderAdvertisement = (value) => parseEveContract("providerAdvertisement", value); export const parseEveSurfaceDocument = (value) => parseEveContract("surface", value); export const parseEveCommandDescriptor = (value) => parseEveContract("commandDescriptor", value); export const parseEveCommandInvocation = (value) => parseEveContract("commandInvocation", value); export const parseEveCommandReceipt = (value) => parseEveContract("commandReceipt", value); +export const parseEveInputCapability = (value) => parseEveContract("inputCapability", value); function requiredValidator(schemaId) { const validator = ajv.getSchema(schemaId); if (!validator) diff --git a/packages/eve-contracts/scripts/generate-contracts.mjs b/packages/eve-contracts/scripts/generate-contracts.mjs index a3138e0..a8be632 100644 --- a/packages/eve-contracts/scripts/generate-contracts.mjs +++ b/packages/eve-contracts/scripts/generate-contracts.mjs @@ -13,6 +13,7 @@ const contracts = [ ["commandDescriptor", "EveCommandDescriptor", "command-descriptor.ts", "gamecult.eve.command.v1.schema.json"], ["commandInvocation", "EveCommandInvocation", "command-invocation.ts", "gamecult.eve.command_invocation.v1.schema.json"], ["commandReceipt", "EveCommandReceipt", "command-receipt.ts", "gamecult.eve.command_receipt.v1.schema.json"], + ["inputCapability", "EveInputCapability", "input-capability.ts", "gamecult.eve.input_capability.v1.schema.json"], ]; const loaded = []; diff --git a/packages/eve-contracts/src/generated/input-capability.ts b/packages/eve-contracts/src/generated/input-capability.ts new file mode 100644 index 0000000..ea3f862 --- /dev/null +++ b/packages/eve-contracts/src/generated/input-capability.ts @@ -0,0 +1,54 @@ +// Generated by scripts/generate-contracts.mjs. Do not edit by hand. +// Source: schemas/gamecult.eve.input_capability.v1.schema.json +export type InputValue = ({ +[k: string]: any +} & { +model: ("button-hold.v1" | "axis.v1" | "view-direction.v1") +payloadKey?: string +/** + * @minItems 3 + * @maxItems 3 + */ +payloadKeys?: [string, string, string] +}) + +export interface EveInputCapability { +schema: "gamecult.eve.input_capability.v1" +providerId: string +capabilityId: string +version?: number +actions: { +actionId: string +label: string +operation: string +context?: string +category?: string +iconRef?: string +availability: ("available" | "dormant" | "unavailable") +sourceRef?: string +payload?: { +[k: string]: string +} +inputValue?: InputValue +}[] +defaultProfiles: { +profileId: string +deviceClass: ("keyboard-mouse" | "gamepad") +bindings: Binding[] +}[] +} +export interface Binding { +bindingId: string +actionId: string +gesture: Gesture +actionBar?: boolean +} +export interface Gesture { +kind: ("direct" | "chord" | "sequence" | "axis") +/** + * @minItems 1 + */ +controls: [string, ...(string)[]] +maxStepIntervalMs?: number +completionControl?: string +} diff --git a/packages/eve-contracts/src/generated/schemas.ts b/packages/eve-contracts/src/generated/schemas.ts index 1ed9031..10d57b2 100644 --- a/packages/eve-contracts/src/generated/schemas.ts +++ b/packages/eve-contracts/src/generated/schemas.ts @@ -748,6 +748,235 @@ export const eveContractSchemas = { } }, "additionalProperties": true + }, + "inputCapability": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "gamecult.eve.input_capability.v1", + "title": "Eve Input Capability", + "type": "object", + "required": [ + "schema", + "providerId", + "capabilityId", + "actions", + "defaultProfiles" + ], + "properties": { + "schema": { + "const": "gamecult.eve.input_capability.v1" + }, + "providerId": { + "type": "string", + "minLength": 1 + }, + "capabilityId": { + "type": "string", + "minLength": 1 + }, + "version": { + "type": "integer", + "minimum": 0 + }, + "actions": { + "type": "array", + "items": { + "type": "object", + "required": [ + "actionId", + "label", + "operation", + "availability" + ], + "properties": { + "actionId": { + "type": "string", + "minLength": 1 + }, + "label": { + "type": "string", + "minLength": 1 + }, + "operation": { + "type": "string", + "minLength": 1 + }, + "context": { + "type": "string" + }, + "category": { + "type": "string" + }, + "iconRef": { + "type": "string" + }, + "availability": { + "enum": [ + "available", + "dormant", + "unavailable" + ] + }, + "sourceRef": { + "type": "string" + }, + "payload": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "inputValue": { + "$ref": "#/$defs/inputValue" + } + }, + "additionalProperties": false + } + }, + "defaultProfiles": { + "type": "array", + "items": { + "type": "object", + "required": [ + "profileId", + "deviceClass", + "bindings" + ], + "properties": { + "profileId": { + "type": "string", + "minLength": 1 + }, + "deviceClass": { + "enum": [ + "keyboard-mouse", + "gamepad" + ] + }, + "bindings": { + "type": "array", + "items": { + "$ref": "#/$defs/binding" + } + } + }, + "additionalProperties": false + } + } + }, + "$defs": { + "inputValue": { + "type": "object", + "required": [ + "model" + ], + "properties": { + "model": { + "enum": [ + "button-hold.v1", + "axis.v1", + "view-direction.v1" + ] + }, + "payloadKey": { + "type": "string", + "minLength": 1 + }, + "payloadKeys": { + "type": "array", + "minItems": 3, + "maxItems": 3, + "items": { + "type": "string", + "minLength": 1 + } + } + }, + "allOf": [ + { + "if": { + "properties": { + "model": { + "const": "view-direction.v1" + } + }, + "required": [ + "model" + ] + }, + "then": { + "required": [ + "payloadKeys" + ] + }, + "else": { + "required": [ + "payloadKey" + ] + } + } + ], + "additionalProperties": false + }, + "gesture": { + "type": "object", + "required": [ + "kind", + "controls" + ], + "properties": { + "kind": { + "enum": [ + "direct", + "chord", + "sequence", + "axis" + ] + }, + "controls": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "minLength": 1 + } + }, + "maxStepIntervalMs": { + "type": "integer", + "minimum": 50 + }, + "completionControl": { + "type": "string" + } + }, + "additionalProperties": false + }, + "binding": { + "type": "object", + "required": [ + "bindingId", + "actionId", + "gesture" + ], + "properties": { + "bindingId": { + "type": "string", + "minLength": 1 + }, + "actionId": { + "type": "string", + "minLength": 1 + }, + "gesture": { + "$ref": "#/$defs/gesture" + }, + "actionBar": { + "type": "boolean" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false } } as const; diff --git a/packages/eve-contracts/src/index.ts b/packages/eve-contracts/src/index.ts index b3b103d..518297d 100644 --- a/packages/eve-contracts/src/index.ts +++ b/packages/eve-contracts/src/index.ts @@ -2,6 +2,7 @@ import Ajv2020, { type ErrorObject, type ValidateFunction } from "ajv/dist/2020. import type { EveCommandDescriptor } from "./generated/command-descriptor.js"; import type { EveCommandInvocation } from "./generated/command-invocation.js"; import type { EveCommandReceipt } from "./generated/command-receipt.js"; +import type { EveInputCapability } from "./generated/input-capability.js"; import type { EveProviderAdvertisement } from "./generated/provider-advertisement.js"; import type { EveSurfaceDocument } from "./generated/surface.js"; import { eveContractSchemas, type EveContractSchemaName } from "./generated/schemas.js"; @@ -9,6 +10,7 @@ import { eveContractSchemas, type EveContractSchemaName } from "./generated/sche export type { EveCommandDescriptor } from "./generated/command-descriptor.js"; export type { EveCommandInvocation } from "./generated/command-invocation.js"; export type { EveCommandReceipt } from "./generated/command-receipt.js"; +export type { EveInputCapability } from "./generated/input-capability.js"; export type { EveProviderAdvertisement } from "./generated/provider-advertisement.js"; export type { EveSurfaceDocument } from "./generated/surface.js"; export { eveContractSchemas, type EveContractSchemaName } from "./generated/schemas.js"; @@ -18,6 +20,7 @@ export const EVE_SURFACE_SCHEMA = "gamecult.eve.surface.v1" as const; export const EVE_COMMAND_DESCRIPTOR_SCHEMA = "gamecult.eve.command.v1" as const; export const EVE_COMMAND_INVOCATION_SCHEMA = "gamecult.eve.command_invocation.v1" as const; export const EVE_COMMAND_RECEIPT_SCHEMA = "gamecult.eve.command_receipt.v1" as const; +export const EVE_INPUT_CAPABILITY_SCHEMA = "gamecult.eve.input_capability.v1" as const; type ContractTypes = { providerAdvertisement: EveProviderAdvertisement; @@ -25,6 +28,7 @@ type ContractTypes = { commandDescriptor: EveCommandDescriptor; commandInvocation: EveCommandInvocation; commandReceipt: EveCommandReceipt; + inputCapability: EveInputCapability; }; const ajv = new Ajv2020({ @@ -78,6 +82,8 @@ export const isEveCommandInvocation = (value: unknown): value is EveCommandInvoc isEveContract("commandInvocation", value); export const isEveCommandReceipt = (value: unknown): value is EveCommandReceipt => isEveContract("commandReceipt", value); +export const isEveInputCapability = (value: unknown): value is EveInputCapability => + isEveContract("inputCapability", value); export const parseEveProviderAdvertisement = (value: unknown): EveProviderAdvertisement => parseEveContract("providerAdvertisement", value); @@ -89,6 +95,8 @@ export const parseEveCommandInvocation = (value: unknown): EveCommandInvocation parseEveContract("commandInvocation", value); export const parseEveCommandReceipt = (value: unknown): EveCommandReceipt => parseEveContract("commandReceipt", value); +export const parseEveInputCapability = (value: unknown): EveInputCapability => + parseEveContract("inputCapability", value); function requiredValidator(schemaId: string): ValidateFunction { const validator = ajv.getSchema(schemaId); diff --git a/packages/eve-contracts/test/contracts.test.mjs b/packages/eve-contracts/test/contracts.test.mjs index 2f31bfe..01155f9 100644 --- a/packages/eve-contracts/test/contracts.test.mjs +++ b/packages/eve-contracts/test/contracts.test.mjs @@ -7,6 +7,7 @@ import { isEveCommandDescriptor, parseEveCommandInvocation, parseEveCommandReceipt, + parseEveInputCapability, parseEveProviderAdvertisement, parseEveSurfaceDocument, } from "../dist/index.js"; @@ -86,6 +87,49 @@ test("published schema constants retain their authoritative ids", async () => { assert.equal(schemas.$id, "gamecult.eve.surface.v1"); }); +test("validates scalar and view-direction input value models", () => { + const base = { + schema: "gamecult.eve.input_capability.v1", + providerId: "flight.provider", + capabilityId: "pilot.input", + version: 1, + defaultProfiles: [], + }; + assert.equal(parseEveInputCapability({ + ...base, + actions: [{ + actionId: "pilot.fire", + label: "Fire", + operation: "pilot.fire", + availability: "available", + inputValue: { model: "button-hold.v1", payloadKey: "active" }, + }], + }).actions[0].inputValue.payloadKey, "active"); + assert.deepEqual(parseEveInputCapability({ + ...base, + actions: [{ + actionId: "pilot.target-reticle", + label: "Target Reticle", + operation: "pilot.target-reticle", + availability: "available", + inputValue: { + model: "view-direction.v1", + payloadKeys: ["directionX", "directionY", "directionZ"], + }, + }], + }).actions[0].inputValue.payloadKeys, ["directionX", "directionY", "directionZ"]); + assert.throws(() => parseEveInputCapability({ + ...base, + actions: [{ + actionId: "pilot.target-reticle", + label: "Target Reticle", + operation: "pilot.target-reticle", + availability: "available", + inputValue: { model: "view-direction.v1", payloadKey: "directionX" }, + }], + }), EveContractValidationError); +}); + test("validates an existing canonical Eve surface fixture", async () => { const fixturePath = resolve( import.meta.dirname, diff --git a/schemas/gamecult.eve.input_capability.v1.schema.json b/schemas/gamecult.eve.input_capability.v1.schema.json index eb14248..8dd7918 100644 --- a/schemas/gamecult.eve.input_capability.v1.schema.json +++ b/schemas/gamecult.eve.input_capability.v1.schema.json @@ -46,11 +46,24 @@ "$defs": { "inputValue": { "type": "object", - "required": ["model", "payloadKey"], + "required": ["model"], "properties": { - "model": { "enum": ["button-hold.v1", "axis.v1"] }, - "payloadKey": { "type": "string", "minLength": 1 } + "model": { "enum": ["button-hold.v1", "axis.v1", "view-direction.v1"] }, + "payloadKey": { "type": "string", "minLength": 1 }, + "payloadKeys": { + "type": "array", + "minItems": 3, + "maxItems": 3, + "items": { "type": "string", "minLength": 1 } + } }, + "allOf": [ + { + "if": { "properties": { "model": { "const": "view-direction.v1" } }, "required": ["model"] }, + "then": { "required": ["payloadKeys"] }, + "else": { "required": ["payloadKey"] } + } + ], "additionalProperties": false }, "gesture": {