UNPKG

bevy-remote-protocol

Version:
384 lines (356 loc) 14.2 kB
// Bevy Remote Protocol Client // https://docs.rs/bevy/latest/bevy/remote/index.html import { URL } from 'url'; import { BrpResponse, EntityId, TypePath, BrpGetWatchResult, BrpGetWatchStrictResult, BrpListWatchResult, ServerVersion, BrpObject, BrpErrors, } from './types'; import { TextDecoder } from 'util'; function reverseMap(map: Record<string, string>): Record<string, string> { const result: Record<string, string> = {}; for (const key in map) { const value = map[key]; result[value] = key; } return result; } function collectMaps(maps: Array<Record<string, string>>) { const collection: Record<string, string> = {}; for (const map of maps) { for (const key in map) { collection[key] = map[key]; } } return collection; } export class BevyRemoteProtocol { static DEFAULT_URL = new URL('http://127.0.0.1:15702'); static INTERNAL_TO_V0_15: Record<string, string> = { 'bevy_ecs::hierarchy::Children': 'bevy_hierarchy::components::children::Children', 'bevy_ecs::name::Name': 'bevy_core::name::Name', }; static INTERNAL_TO_V0_16: Record<string, string> = {}; static ANY_TO_INTERNAL = collectMaps([reverseMap(this.INTERNAL_TO_V0_15), reverseMap(this.INTERNAL_TO_V0_16)]); private static decoder = new TextDecoder(); private id: number; public url: URL; public serverVersion: ServerVersion; constructor(url: URL, version: ServerVersion) { this.id = 0; this.url = url; this.serverVersion = version; } private nextId() { return this.id++; // starting from 0 } private translateToInternal(message: string): string { const table = (() => { if (this.serverVersion === ServerVersion.IGNORE) return {}; return BevyRemoteProtocol.ANY_TO_INTERNAL; })(); for (const search in table) { message = message.replace(new RegExp(search, 'g'), table[search]); } return message; } private translateToSpecificVersion(message: string): string { const table = (() => { switch (this.serverVersion) { case ServerVersion.V0_15: return BevyRemoteProtocol.INTERNAL_TO_V0_15; case ServerVersion.V0_16: return BevyRemoteProtocol.INTERNAL_TO_V0_16; default: return {}; // ignore } })(); for (const search in table) { message = message.replace(new RegExp(search, 'g'), table[search]); } return message; } private requestWrapper(rpcMethod: string, rpcParams: any, signal?: AbortSignal): RequestInit { return { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json', }, body: this.translateToSpecificVersion( JSON.stringify({ jsonrpc: '2.0', id: this.nextId(), method: rpcMethod, params: rpcParams, }) ), signal, }; } private async request<R>(method: string, params: any): Promise<BrpResponse<R>> { // throws error if connection refused by url return await JSON.parse( this.translateToInternal(await (await fetch(this.url, this.requestWrapper(method, params))).text()) ); } private async requestStream<R>( method: string, params: any, signal: AbortSignal, observer: (arg: R) => void ): Promise<null> { // throws error if connection refused by url const response = await fetch(this.url, this.requestWrapper(method, params, signal)); if (!response.body) return null; try { // https://developer.mozilla.org/en-US/docs/Web/API/Streams_API/Using_readable_streams for await (const chunk of response.body) { const decoded: string = BevyRemoteProtocol.decoder.decode(chunk); const translated = this.translateToInternal(decoded); const parsed = JSON.parse(translated.slice(decoded.indexOf('{'))); if (parsed.result) observer(parsed.result); } } catch (error) {} return null; } /** * Retrieve the values of one or more components from an entity. * * This function passes a flag `strict` as `false` by default. * * `params`: * - `entity`: The ID of the entity whose components will be fetched. * - `components`: An array of [fully-qualified type names] of components to fetch. * - `strict` (optional): A flag to enable strict mode which will fail if any one of the * components is not present or can not be reflected. * * `result`: * - `components`: A map associating each type name to its value on the requested entity. * - `errors`: A map associating each type name with an error if it was not on the entity * or could not be reflected. */ public async get( entity: EntityId, components: TypePath[] ): Promise<BrpResponse<{ components: BrpObject; errors: BrpErrors }>> { return this.request('bevy/get', { entity, components, strict: false }); } /** * Retrieve the values of one or more components from an entity. * * This function passes a flag `strict` as `true` by default. * * `params`: * - `entity`: The ID of the entity whose components will be fetched. * - `components`: An array of [fully-qualified type names] of components to fetch. * - `strict` (optional): A flag to enable strict mode which will fail if any one of the * components is not present or can not be reflected. * * `result`: A map associating each type name to its value on the requested entity. */ public async get_strict(entity: EntityId, components: TypePath[]): Promise<BrpResponse<BrpObject>> { return this.request('bevy/get', { entity, components, strict: true }); } /** * Perform a query over components in the ECS, returning all matching entities and their associated * component values. * * All of the arrays that comprise this request are optional, and when they are not provided, they * will be treated as if they were empty. * * `params`: * - `components` (optional): An array of [fully-qualified type names] of components to fetch. * - `option` (optional): An array of fully-qualified type names of components to fetch optionally. * - `has` (optional): An array of fully-qualified type names of components whose presence will be * reported as boolean values. * - `with` (optional): An array of fully-qualified type names of components that must be present * on entities in order for them to be included in results. * - `without` (optional): An array of fully-qualified type names of components that must *not* be * present on entities in order for them to be included in results. * * `result`: An array, each of which is an object containing: * - `entity`: The ID of a query-matching entity. * - `components`: A map associating each type name from `components`/`option` to its value on the matching * entity if the component is present. * - `has`: A map associating each type name from `has` to a boolean value indicating whether or not the * entity has that component. If `has` was empty or omitted, this key will be omitted in the response. */ public async query({ components, option, has, filterWith, filterWithout, }: { components?: TypePath[]; option?: TypePath[]; has?: TypePath[]; filterWith?: TypePath[]; filterWithout?: TypePath[]; }): Promise<BrpResponse<[{ entity: EntityId; components: BrpObject; has: { [key: TypePath]: boolean } }]>> { return this.request('bevy/query', { data: { components, option, has }, filter: { with: filterWith, without: filterWithout }, }); } /** * Create a new entity with the provided components and return the resulting entity ID. * * `params`: * - `components`: A map associating each component's [fully-qualified type name] with its value. * * `result`: * - `entity`: The ID of the newly spawned entity. */ public async spawn(components: BrpObject): Promise<BrpResponse<{ entity: EntityId }>> { return this.request('bevy/spawn', { components }); } /** * Despawn the entity with the given ID. * * `params`: * - `entity`: The ID of the entity to be despawned. * * `result`: null. */ public async destroy(entity: EntityId): Promise<BrpResponse<null>> { return this.request('bevy/destroy', { entity }); } /** * Delete one or more components from an entity. * * `params`: * - `entity`: The ID of the entity whose components should be removed. * - `components`: An array of [fully-qualified type names] of components to be removed. * * `result`: null. */ public async remove(entity: EntityId, components: TypePath[]): Promise<BrpResponse<null>> { return this.request('bevy/remove', { entity, components }); } /** * Insert one or more components into an entity. * * `params`: * - `entity`: The ID of the entity to insert components into. * - `components`: A map associating each component's fully-qualified type name with its value. * * `result`: null. */ public async insert(entity: EntityId, components: BrpObject): Promise<BrpResponse<null>> { return this.request('bevy/insert', { entity, components }); } /** * Assign a new parent to one or more entities. * * `params`: * - `entities`: An array of entity IDs of entities that will be made children of the `parent`. * - `parent` (optional): The entity ID of the parent to which the child entities will be assigned. * If excluded, the given entities will be removed from their parents. * * `result`: null. */ public async reparent(entities: EntityId[], parent?: EntityId): Promise<BrpResponse<null>> { return this.request('bevy/reparent', { entities, parent }); } /** * List all registered components or all components present on an entity. * * When `params` is not provided, this lists all registered components. If `params` is provided, * this lists only those components present on the provided entity. * * `params` (optional): * - `entity`: The ID of the entity whose components will be listed. * * `result`: An array of fully-qualified type names of components. */ public async list(entity?: EntityId): Promise<BrpResponse<TypePath[]>> { if (entity) return this.request('bevy/list', { entity }); return this.request('bevy/list', null); } /** * Watch the values of one or more components from an entity. * * This function passes a flag `strict` as `false` by default. * * `params`: * - `entity`: The ID of the entity whose components will be fetched. * - `components`: An array of [fully-qualified type names] of components to fetch. * - `signal`: A signal object that allows you to abort it if required via an {@link AbortController} object. * - `observer`: A handler of chunks of Response. * * `result`: * - `components`: A map of components added or changed in the last tick associating each type * name to its value on the requested entity. * - `removed`: An array of fully-qualified type names of components removed from the entity * in the last tick. * - `errors`: A map associating each type name with an error if it was not on the entity * or could not be reflected. */ public async get_watch( entity: EntityId, components: TypePath[], signal: AbortSignal, observer: (arg: BrpGetWatchResult) => void ): Promise<null> { return this.requestStream('bevy/get+watch', { entity, components, strict: false }, signal, observer); } /** * Watch the values of one or more components from an entity. * * This function passes a flag `strict` as `true` by default. * Response will fail if any one of the components is not present or can not be refleceted. * * `params`: * - `entity`: The ID of the entity whose components will be fetched. * - `components`: An array of [fully-qualified type names] of components to fetch. * - `signal`: A signal object that allows you to abort it if required via an {@link AbortController} object. * - `observer`: A handler of chunks of Response. * * `result`: * - `components`: A map of components added or changed in the last tick associating each type * name to its value on the requested entity. * - `removed`: An array of fully-qualified type names of components removed from the entity * in the last tick. */ public async get_watch_strict( entity: EntityId, components: TypePath[], signal: AbortSignal, observer: (arg: BrpGetWatchStrictResult) => void ): Promise<null> { return this.requestStream('bevy/get+watch', { entity, components, strict: true }, signal, observer); } /** * Watch all components present on an entity. * * When `entity` is not provided, this lists all registered components. If `entity` is provided, * this lists only those components present on the provided entity. * * `params`: * - `signal`: A signal object that allows you to abort it if required via an {@link AbortController} object. * - `observer`: A handler of chunks of Response. * - `entity`: (optional) The ID of the entity whose components will be listed. * * `result`: * - `added`: An array of fully-qualified type names of components added to the entity in the * last tick. * - `removed`: An array of fully-qualified type names of components removed from the entity * in the last tick. */ public async list_watch( signal: AbortSignal, observer: (arg: BrpListWatchResult) => void, entity?: EntityId ): Promise<null> { if (entity) return this.requestStream('bevy/list+watch', { entity }, signal, observer); return this.requestStream('bevy/list+watch', null, signal, observer); } }