bevy-remote-protocol
Version:
Bevy Remote Protocol Client
384 lines (356 loc) • 14.2 kB
text/typescript
// 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);
}
}