diff --git a/package-lock.json b/package-lock.json index 99cc70a9d..386f39093 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2426,7 +2426,7 @@ }, "duplexer": { "version": "0.1.1", - "resolved": "http://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=", "dev": true }, @@ -2530,7 +2530,7 @@ }, "es6-promisify": { "version": "5.0.0", - "resolved": "http://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", + "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", "dev": true, "requires": { @@ -3278,7 +3278,7 @@ }, "camelcase-keys": { "version": "2.1.0", - "resolved": "http://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", "dev": true, "requires": { @@ -3307,7 +3307,7 @@ }, "load-json-file": { "version": "1.1.0", - "resolved": "http://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", "dev": true, "requires": { @@ -3326,7 +3326,7 @@ }, "meow": { "version": "3.7.0", - "resolved": "http://registry.npmjs.org/meow/-/meow-3.7.0.tgz", + "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", "dev": true, "requires": { @@ -3379,7 +3379,7 @@ }, "pify": { "version": "2.3.0", - "resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", "dev": true }, @@ -3501,7 +3501,7 @@ "dependencies": { "pify": { "version": "2.3.0", - "resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", "dev": true } @@ -4114,7 +4114,7 @@ }, "is-obj": { "version": "1.0.1", - "resolved": "http://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", "dev": true }, @@ -4712,7 +4712,7 @@ }, "mkdirp": { "version": "0.5.1", - "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "requires": { "minimist": "0.0.8" @@ -4837,7 +4837,7 @@ "dependencies": { "semver": { "version": "5.3.0", - "resolved": "http://registry.npmjs.org/semver/-/semver-5.3.0.tgz", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=", "dev": true } @@ -5641,7 +5641,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, "requires": { @@ -6480,7 +6480,7 @@ }, "through": { "version": "2.3.8", - "resolved": "http://registry.npmjs.org/through/-/through-2.3.8.tgz", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", "dev": true }, @@ -6951,7 +6951,7 @@ }, "wrap-ansi": { "version": "2.1.0", - "resolved": "http://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", "dev": true, "requires": { diff --git a/packages/gltf-gen/package-lock.json b/packages/gltf-gen/package-lock.json index 0da17ed24..eea76e696 100644 --- a/packages/gltf-gen/package-lock.json +++ b/packages/gltf-gen/package-lock.json @@ -1048,7 +1048,7 @@ "dependencies": { "minimist": { "version": "0.0.10", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=", "dev": true } diff --git a/packages/sdk/src/types/runtime/actor.ts b/packages/sdk/src/actor/actor.ts similarity index 95% rename from packages/sdk/src/types/runtime/actor.ts rename to packages/sdk/src/actor/actor.ts index d726624d1..703a28e49 100644 --- a/packages/sdk/src/types/runtime/actor.ts +++ b/packages/sdk/src/actor/actor.ts @@ -1,926 +1,929 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -import events from 'events'; -import { - ActorTransform, - ActorTransformLike, - Appearance, - AppearanceLike, - Asset, - AssetContainer, - Attachment, - AttachmentLike, - AttachPoint, - Collider, - ColliderLike, - ColliderType, - CollisionLayer, - Light, - LightLike, - LookAt, - LookAtLike, - Prefab, - RigidBody, - RigidBodyLike, - Text, - TextLike, - User, -} from '.'; -import { - Animation, - Context, - CreateAnimationOptions, - Guid, - LookAtMode, - PrimitiveDefinition, - ReadonlyMap, - SetAnimationStateOptions, - SetAudioStateOptions, - SetVideoStateOptions, - Vector3Like, - ZeroGuid, -} from '../..'; - -import { log } from '../../log'; -import { observe, unobserve } from '../../utils/observe'; -import readPath from '../../utils/readPath'; -import resolveJsonValues from '../../utils/resolveJsonValues'; -import { InternalActor } from '../internal/actor'; -import { SubscriptionType } from '../network/subscriptionType'; -import { Patchable } from '../patchable'; -import { ActionHandler, ActionState, Behavior, DiscreteAction } from './behaviors'; -import { MediaInstance } from './mediaInstance'; -import { ColliderGeometry } from './physics'; - -/** - * Describes the properties of an Actor. - */ -export interface ActorLike { - id: Guid; - parentId: Guid; - name: string; - tag: string; - - /** - * When supplied, this actor will be unsynchronized, and only exist on the client - * of the User with the given ID. This value can only be set at actor creation. - * Any actors parented to this actor will also be exclusive to the given user. - */ - exclusiveToUser: Guid; - subscriptions: SubscriptionType[]; - transform: Partial; - appearance: Partial; - light: Partial; - rigidBody: Partial; - collider: Partial; - text: Partial; - attachment: Partial; - lookAt: Partial; - grabbable: boolean; -} - -/** - * An actor represents an object instantiated on the host. - */ -export class Actor implements ActorLike, Patchable { - private _internal = new InternalActor(this); - /** @hidden */ - public get internal() { return this._internal; } - - private _emitter = new events.EventEmitter(); - /** @hidden */ - public get emitter() { return this._emitter; } - - private _name: string; - private _tag: string; - private _exclusiveToUser: Guid; - private _parentId = ZeroGuid; - private _subscriptions: SubscriptionType[] = []; - private _transform = new ActorTransform(); - private _appearance = new Appearance(this); - private _light: Light; - private _rigidBody: RigidBody; - private _collider: Collider; - private _text: Text; - private _attachment: Attachment; - private _lookAt: LookAt; - private _grabbable = false; - private _grab: DiscreteAction; - - private get grab() { this._grab = this._grab || new DiscreteAction(); return this._grab; } - - /* - * PUBLIC ACCESSORS - */ - - public get context() { return this._context; } - public get id() { return this._id; } - public get name() { return this._name; } - public get tag() { return this._tag; } - public set tag(value) { this._tag = value; this.actorChanged('tag'); } - - /** @inheritdoc */ - public get exclusiveToUser() { return this._exclusiveToUser; } - public get subscriptions() { return this._subscriptions; } - public get transform() { return this._transform; } - public set transform(value) { this._transform.copy(value); } - public get appearance() { return this._appearance; } - public set appearance(value) { this._appearance.copy(value); } - public get light() { return this._light; } - public get rigidBody() { return this._rigidBody; } - public get collider() { return this._collider; } - public get text() { return this._text; } - public get attachment() { return this._attachment; } - public get lookAt() { return this._lookAt; } - public get children() { return this.context.actors.filter(actor => actor.parentId === this.id); } - public get parent() { return this._context.actor(this._parentId); } - public set parent(value) { this.parentId = value && value.id || ZeroGuid; } - public get parentId() { return this._parentId; } - public set parentId(value) { - const parentActor = this.context.actor(value); - if (!value || !parentActor) { - value = ZeroGuid; - } - if (parentActor && parentActor.exclusiveToUser && parentActor.exclusiveToUser !== this.exclusiveToUser) { - throw new Error(`User-exclusive actor ${this.id} can only be parented to inclusive actors ` + - "and actors that are exclusive to the same user."); - } - if (this._parentId !== value) { - this._parentId = value; - this.actorChanged('parentId'); - } - } - - public get grabbable() { return this._grabbable; } - public set grabbable(value) { - if (value !== this._grabbable) { - this._grabbable = value; - this.actorChanged('grabbable'); - } - } - - private constructor(private _context: Context, private _id: Guid) { - // Actor patching: Observe the transform for changed values. - observe({ - target: this._transform, - targetName: 'transform', - notifyChanged: (...path: string[]) => this.actorChanged(...path) - }); - - // Observe changes to the looks of this actor - observe({ - target: this._appearance, - targetName: 'appearance', - notifyChanged: (...path: string[]) => this.actorChanged(...path) - }); - } - - /** - * @hidden - * TODO - get rid of this. - */ - public static alloc(context: Context, id: Guid): Actor { - return new Actor(context, id); - } - - /** - * PUBLIC METHODS - */ - - /** - * Creates a new, empty actor without geometry. - * @param context The SDK context object. - * @param options.actor The initial state of the actor. - */ - public static Create(context: Context, options?: { - actor?: Partial; - }): Actor { - return context.internal.Create(options); - } - - /** - * @deprecated - * Use [[Actor.Create]] instead. - */ - public static CreateEmpty(context: Context, options?: { - actor?: Partial; - }): Actor { - return Actor.Create(context, options); - } - - /** - * Creates a new actor from a library resource. - * Host-specific list of library resources. For AltspaceVR, see: https://account.altvr.com/kits - * @param context The SDK context object. - * @param options.resourceId The id of the library resource to instantiate. - * @param options.actor The initial state of the root actor. - */ - public static CreateFromLibrary(context: Context, options: { - resourceId: string; - actor?: Partial; - }): Actor { - return context.internal.CreateFromLibrary(options); - } - - /** - * Creates a new actor hierarchy from the provided prefab. - * @param context The SDK context object. - * @param options.prefabId The ID of a prefab asset to spawn. - * @param options.collisionLayer If the prefab contains colliders, put them on this layer. - * @param options.actor The initial state of the root actor. - */ - public static CreateFromPrefab(context: Context, options: { - prefabId: Guid; - collisionLayer?: CollisionLayer; - actor?: Partial; - }): Actor; - - /** - * Creates a new actor hierarchy from the provided prefab. - * @param context The SDK context object. - * @param options.prefab The prefab asset to spawn. - * @param options.collisionLayer If the prefab contains colliders, put them on this layer. - * @param options.actor The initial state of the root actor. - */ - public static CreateFromPrefab(context: Context, options: { - prefab: Prefab; - collisionLayer?: CollisionLayer; - actor?: Partial; - }): Actor; - - /** - * Creates a new actor hierarchy from the provided prefab. - * @param context The SDK context object. - * @param options.firstPrefabFrom An asset array containing at least one prefab. - * @param options.collisionLayer If the prefab contains colliders, put them on this layer. - * @param options.actor The initial state of the root actor. - */ - public static CreateFromPrefab(context: Context, options: { - firstPrefabFrom: Asset[]; - collisionLayer?: CollisionLayer; - actor?: Partial; - }): Actor; - - public static CreateFromPrefab(context: Context, options: { - prefabId?: Guid; - prefab?: Prefab; - firstPrefabFrom?: Asset[]; - collisionLayer?: CollisionLayer; - actor?: Partial; - }): Actor { - let prefabId = options.prefabId; - if (!prefabId && options.prefab) { - prefabId = options.prefab.id; - } - if (!prefabId && options.firstPrefabFrom) { - prefabId = options.firstPrefabFrom.find(a => !!a.prefab).id; - } - if (!prefabId) { - throw new Error("No prefab supplied to CreateFromPrefab"); - } - - return context.internal.CreateFromPrefab({ - prefabId, - collisionLayer: options.collisionLayer, - actor: options.actor - }); - } - - /** - * Load a glTF model, and spawn the first prefab in the resulting assets. Equivalent - * to using [[AssetContainer.loadGltf]] and [[Actor.CreateFromPrefab]]. - * @param container The asset container to load the glTF assets into - * @param options.uri A URI to a .gltf or .glb file - * @param options.colliderType The type of collider to add to each mesh actor - * @param options.actor The initial state of the actor - */ - public static CreateFromGltf(container: AssetContainer, options: { - uri: string; - colliderType?: 'box' | 'mesh'; - actor?: Partial; - }): Actor { - return container.context.internal.CreateFromGltf(container, options); - } - - /** - * Create an actor with a newly generated mesh. Equivalent to using - * [[AssetContainer.createPrimitiveMesh]] and adding the result to [[Actor.Create]]. - * @param container The asset container to load the mesh into - * @param options.definition The primitive shape and size - * @param options.addCollider Add an auto-typed collider to the actor - * @param options.actor The initial state of the actor - */ - public static CreatePrimitive(container: AssetContainer, options: { - definition: PrimitiveDefinition; - addCollider?: boolean; - actor?: Partial; - }): Actor { - const actor = options.actor || {}; - const mesh = container.createPrimitiveMesh(actor.name, options.definition); - return Actor.Create(container.context, { - actor: { - ...actor, - appearance: { - ...actor.appearance, - meshId: mesh.id - }, - collider: options.addCollider - ? actor.collider || { geometry: { shape: ColliderType.Auto } } - : actor.collider - } - }); - } - - /** - * Creates a Promise that will resolve once the actor is created on the host. - * @returns Promise - */ - public created(): Promise { - if (!this.internal.created) { - return new Promise((resolve, reject) => this.internal.enqueueCreatedPromise({ resolve, reject })); - } - if (this.internal.created.success) { - return Promise.resolve(); - } else { - return Promise.reject(this.internal.created.reason); - } - } - - /** - * Destroys the actor. - */ - public destroy(): void { - this.context.internal.destroyActor(this.id); - } - - /** - * Adds a light component to the actor. - * @param light Light characteristics. - */ - public enableLight(light?: Partial) { - if (!this._light) { - this._light = new Light(); - // Actor patching: Observe the light component for changed values. - observe({ - target: this._light, - targetName: 'light', - notifyChanged: (...path: string[]) => this.actorChanged(...path), - // Trigger notifications for every observed leaf node to ensure we get all values in the initial patch. - triggerNotificationsNow: true - }); - } - // Copying the new values will trigger an actor update and enable/update the light component. - this._light.copy(light); - } - - /** - * Adds a rigid body component to the actor. - * @param rigidBody Rigid body characteristics. - */ - public enableRigidBody(rigidBody?: Partial) { - if (!this._rigidBody) { - this._rigidBody = new RigidBody(this); - // Actor patching: Observe the rigid body component for changed values. - observe({ - target: this._rigidBody, - targetName: 'rigidBody', - notifyChanged: (...path: string[]) => this.actorChanged(...path), - // Trigger notifications for every observed leaf node to ensure we get all values in the initial patch. - triggerNotificationsNow: true - }); - } - // Copying the new values will trigger an actor update and enable/update the rigid body component. - this._rigidBody.copy(rigidBody); - } - - /** - * Adds a collider of the given type and parameters on the actor. - * @param colliderType Type of the collider to enable. - * @param isTrigger Whether the collider is a trigger volume or not. - * @param radius The radius of the collider. If omitted, a best-guess radius is chosen - * based on the size of the currently assigned mesh (loading meshes are not considered). - * If no mesh is assigned, defaults to 0.5. - * @param center The center of the collider, or default of the object if none is provided. - */ - // * @param collisionLayer The layer that the collider operates in. - public setCollider( - colliderType: ColliderType.Sphere, - // collisionLayer: CollisionLayer, - isTrigger: boolean, - radius?: number, - center?: Vector3Like - ): void; - - /** - * Adds a collider of the given type and parameters on the actor. - * @param colliderType Type of the collider to enable. - * @param isTrigger Whether the collider is a trigger volume or not. - * @param size The dimensions of the collider. If omitted, a best-guess size is chosen - * based on the currently assigned mesh (loading meshes are not considered). - * If no mesh is assigned, defaults to (1,1,1). - * @param center The center of the collider, or default of the object if none is provided. - */ - public setCollider( - colliderType: ColliderType.Box, - // collisionLayer: CollisionLayer, - isTrigger: boolean, - size?: Vector3Like, - center?: Vector3Like - ): void; - - /** - * Adds a collider of the give type and parameters on the actor. - * @param colliderType Type of the collider to enable. - * @param isTrigger Whether the collider is a trigger volume or not. - * @param size The dimensions of the collider, with the largest component of the vector - * being the primary axis and height of the capsule (including end caps), and the smallest the diameter. - * If omitted, a best-guess size is chosen based on the currently assigned mesh - * (loading meshes are not considered). If no mesh is assigned, defaults to (1, 1, 1). - * @param center The center of the collider, or default of the object if none is provided. - */ - public setCollider( - colliderType: ColliderType.Capsule, - isTrigger: boolean, - size?: Vector3Like, - center?: Vector3Like - ): void; - - /** - * Adds a collider whose shape is determined by the current mesh. - * @param colliderType Type of the collider to enable. - * @param isTrigger Whether the collider is a trigger volume or not. - */ - public setCollider( - colliderType: ColliderType.Auto, - isTrigger: boolean - ): void; - - public setCollider( - colliderType: ColliderType, - // collisionLayer: CollisionLayer, - isTrigger: boolean, - size?: number | Vector3Like, - center = { x: 0, y: 0, z: 0 } as Vector3Like - ): void { - const colliderGeometry = this.generateColliderGeometry(colliderType, size, center); - if (colliderGeometry) { - this._setCollider({ - enabled: true, - isTrigger, - // collisionLayer, - geometry: colliderGeometry - } as ColliderLike); - } - } - - /** - * Adds a text component to the actor. - * @param text Text characteristics - */ - public enableText(text?: Partial) { - if (!this._text) { - this._text = new Text(); - // Actor patching: Observe the text component for changed values. - observe({ - target: this._text, - targetName: 'text', - notifyChanged: (...path: string[]) => this.actorChanged(...path), - // Trigger notifications for every observed leaf node to ensure we get all values in the initial patch. - triggerNotificationsNow: true - }); - } - // Copying the new values will trigger an actor update and enable/update the text component. - this._text.copy(text); - } - - /** - * Instruct the actor to face another object, or stop facing an object. - * @param actorOrActorId The Actor or id of the actor to face. - * @param lookAtMode (Optional) How to face the target. @see LookUpMode. - * @param backward (Optional) If true, actor faces away from target rather than toward. - */ - public enableLookAt(actorOrActorId: Actor | Guid, mode?: LookAtMode, backward?: boolean) { - // Resolve the actorId value. - let actorId = ZeroGuid; - if (actorOrActorId instanceof Actor && actorOrActorId.id !== undefined) { - actorId = actorOrActorId.id; - } else if (typeof (actorOrActorId) === 'string') { - actorId = actorOrActorId; - } - // Allocate component if necessary. - if (!this._lookAt) { - this._lookAt = new LookAt(); - // Actor patching: Observe the lookAt component for changed values. - observe({ - target: this._lookAt, - targetName: 'lookAt', - notifyChanged: (...path: string[]) => this.actorChanged(...path), - // Trigger notifications for every observed leaf node to ensure we get all values in the - // initial patch. - triggerNotificationsNow: true - }); - } - // Set component values. - this._lookAt.copy({ - actorId, - mode, - backward - }); - } - - /** - * Attach to the user at the given attach point. - * @param userOrUserId The User or id of user to attach to. - * @param attachPoint Where on the user to attach. - */ - public attach(userOrUserId: User | Guid, attachPoint: AttachPoint) { - const userId = userOrUserId instanceof User ? userOrUserId.id : userOrUserId; - if (!this._attachment) { - // Actor patching: Observe the attachment for changed values. - this._attachment = new Attachment(); - observe({ - target: this._attachment, - targetName: 'attachment', - notifyChanged: (...path: string[]) => this.actorChanged(...path) - }); - } - this._attachment.userId = userId; - this._attachment.attachPoint = attachPoint; - } - - /** - * If attached to a user, detach from it. - */ - public detach() { - this._attachment.userId = ZeroGuid; - this._attachment.attachPoint = 'none'; - } - - /** - * Subscribe to updates from this actor. - * @param subscription The type of subscription to add. - */ - public subscribe(subscription: SubscriptionType) { - this._subscriptions.push(subscription); - this.actorChanged('subscriptions'); - } - - /** - * Unsubscribe from updates from this actor. - * @param subscription The type of subscription to remove. - */ - public unsubscribe(subscription: SubscriptionType) { - this._subscriptions = this._subscriptions.filter(value => value !== subscription); - this.actorChanged('subscriptions'); - } - - /** - * Add a grad handler to be called when the given action state has changed. - * @param grabState The grab state to fire the handler on. - * @param handler The handler to call when the grab state has changed. - */ - public onGrab(grabState: 'begin' | 'end', handler: ActionHandler) { - const actionState: ActionState = (grabState === 'begin') ? 'started' : 'stopped'; - this.grab.on(actionState, handler); - } - - /** - * Sets the behavior on this actor. - * @param behavior The type of behavior to set. Pass null to clear the behavior. - */ - public setBehavior(behavior: { new(): BehaviorT }): BehaviorT { - if (behavior) { - const newBehavior = new behavior(); - this.internal.behavior = newBehavior; - this.context.internal.setBehavior(this.id, this.internal.behavior.behaviorType); - return newBehavior; - } - - this.internal.behavior = null; - this.context.internal.setBehavior(this.id, null); - return null; - } - - /** - * Starts playing a preloaded sound. - * @param soundAssetId Name of sound asset preloaded using AssetManager. - * @param options Adjustments to pitch and volume, and other characteristics. - */ - public startSound( - soundAssetId: Guid, - options: SetAudioStateOptions, - ): MediaInstance { - return new MediaInstance(this, soundAssetId).start(options); - } - - /** - * Starts playing a preloaded video stream. - * @param videoStreamAssetId Name of video stream asset preloaded using AssetManager. - * @param options Adjustments to pitch and volume, and other characteristics. - */ - public startVideoStream( - videoStreamAssetId: Guid, - options: SetVideoStateOptions, - ): MediaInstance { - return new MediaInstance(this, videoStreamAssetId).start(options); - } - - /** - * Creates an animation on the actor. - * @param animationName The name of the animation. - * @param options The animation keyframes, events, and other characteristics. - * @returns A promise resolving to the resulting animation instance. - */ - public createAnimation(animationName: string, options: CreateAnimationOptions) { - return this.context.internal.createAnimation(this.id, animationName, options); - } - - /** - * @deprecated Set [[Animation.isPlaying]] instead. - * Enables the animation on the actor. Animation will start playing immediately. - * @param animationName The name of the animation. - */ - public enableAnimation(animationName: string) { - this.setAnimationState(animationName, { enabled: true }); - } - - /** - * @deprecated Set [[Animation.isPlaying]] instead. - * Disables the animation on the actor. Animation will stop playing immediately. - * When an animation is disabled, it is also paused (its time does not move forward). - * @param animationName The name of the animation. - */ - public disableAnimation(animationName: string) { - this.setAnimationState(animationName, { enabled: false }); - } - - /** - * @deprecated Set [[Animation.isPlaying]] instead. - * Starts the animation (sets animation speed to 1). - * @param animationName The name of the animation. - */ - public resumeAnimation(animationName: string) { - this.setAnimationState(animationName, { enabled: true }); - } - - /** - * @deprecated Set [[Animation.isPlaying]] instead. - * Stops the animation (sets animation speed to zero). - * @param animationName The name of the animation. - */ - public pauseAnimation(animationName: string) { - this.setAnimationState(animationName, { enabled: false }); - } - - /** - * @deprecated Set [[Animation.time]] instead. - * Sets the animation time (units are in seconds). - * @param animationName The name of the animation. - * @param time The desired animation time. A negative value seeks to the end of the animation. - */ - public setAnimationTime(animationName: string, time: number) { - this.setAnimationState(animationName, { time }); - } - - /** - * @deprecated Set properties of an [[Animation]] instance instead. - * (Advanced) Sets the time, speed, and enabled state of an animation. - * @param animationName The name of the animation. - * @param options The time, speed and enabled state to apply. All values are optional. Only the values - * provided will be applied. - */ - public setAnimationState(animationName: string, state: SetAnimationStateOptions) { - return this.context.internal.setAnimationState(this.id, animationName, state); - } - - /** - * Animate actor properties to the given value, following the specified animation curve. Actor transform - * is the only animatable property at the moment. Other properties such as light color may become animatable - * in the future. - * @param value The desired final state of the animation. - * @param duration The length of the interpolation (in seconds). - * @param curve The cubic-bezier curve parameters. @see AnimationEaseCurves for predefined values. - */ - public animateTo(value: Partial, duration: number, curve: number[]) { - this.context.internal.animateTo(this.id, value, duration, curve); - } - - /** - * Finds child actors matching `name`. - * @param name The name of the actors to find. - * @param recurse Whether or not to search recursively. - */ - public findChildrenByName(name: string, recurse: boolean): Actor[] { - const namedChildren = this.children.filter(actor => actor.name === name); - if (!recurse) { - return namedChildren; - } - - for (const child of this.children) { - namedChildren.push(...child.findChildrenByName(name, recurse)); - } - - return namedChildren; - } - - /** - * Actor Events - */ - - /** - * Set an event handler for the animation-disabled event. - * @param handler The handler to call when an animation reaches the end or is otherwise disabled. - */ - public onAnimationDisabled(handler: (animationName: string) => any): this { - this.emitter.addListener('animation-disabled', handler); - return this; - } - - /** - * Set an event handler for the animation-enabled event. - * @param handler The handler to call when an animation moves from the disabled to enabled state. - */ - public onAnimationEnabled(handler: (animationName: string) => any): this { - this.emitter.addListener('animation-enabled', handler); - return this; - } - - /** The list of animations that target this actor, by ID. */ - public get animations() { - return [...this.context.internal.animationSet.values()] - .filter(anim => anim.targetActorIds.includes(this.id)) - .reduce( - (map, anim) => { - map.set(anim.id, anim); - return map; - }, - new Map() - ) as ReadonlyMap; - } - - /** The list of animations that target this actor, by name. */ - public get animationsByName() { - return [...this.context.internal.animationSet.values()] - .filter(anim => anim.targetActorIds.includes(this.id) && anim.name) - .reduce( - (map, anim) => { - map.set(anim.name, anim); - return map; - }, - new Map() - ) as ReadonlyMap; - } - - /** Recursively search for the named animation from this actor. */ - public findAnimationInChildrenByName(name: string): Animation { - if (this.animationsByName.has(name)) { - return this.animationsByName.get(name); - } else { - return this.children.reduce( - (val, child) => val || child.findAnimationInChildrenByName(name), - null as Animation - ); - } - } - - /** @hidden */ - public copy(from: Partial): this { - // Pause change detection while we copy the values into the actor. - const wasObserving = this.internal.observing; - this.internal.observing = false; - - if (!from) { return this; } - if (from.id) { this._id = from.id; } - if (from.parentId) { this._parentId = from.parentId; } - if (from.name) { this._name = from.name; } - if (from.tag) { this._tag = from.tag; } - if (from.exclusiveToUser || from.parentId) { - this._exclusiveToUser = this.parent && this.parent.exclusiveToUser || from.exclusiveToUser; - } - if (from.transform) { this._transform.copy(from.transform); } - if (from.attachment) { this.attach(from.attachment.userId, from.attachment.attachPoint); } - if (from.appearance) { this._appearance.copy(from.appearance); } - if (from.light) { this.enableLight(from.light); } - if (from.rigidBody) { this.enableRigidBody(from.rigidBody); } - if (from.collider) { this._setCollider(from.collider); } - if (from.text) { this.enableText(from.text); } - if (from.lookAt) { this.enableLookAt(from.lookAt.actorId, from.lookAt.mode); } - if (from.grabbable !== undefined) { this._grabbable = from.grabbable; } - - this.internal.observing = wasObserving; - return this; - } - - /** @hidden */ - public toJSON() { - return { - id: this._id, - parentId: this._parentId, - name: this._name, - tag: this._tag, - exclusiveToUser: this._exclusiveToUser, - transform: this._transform.toJSON(), - appearance: this._appearance.toJSON(), - attachment: this._attachment ? this._attachment.toJSON() : undefined, - light: this._light ? this._light.toJSON() : undefined, - rigidBody: this._rigidBody ? this._rigidBody.toJSON() : undefined, - collider: this._collider ? this._collider.toJSON() : undefined, - text: this._text ? this._text.toJSON() : undefined, - lookAt: this._lookAt ? this._lookAt.toJSON() : undefined, - grabbable: this._grabbable - } as ActorLike; - } - - /** - * INTERNAL METHODS - */ - - /** - * Prepare outgoing messages - * @hidden - */ - public static sanitize(msg: ActorLike): ActorLike; - public static sanitize(msg: Partial): Partial; - public static sanitize(msg: ActorLike | Partial): ActorLike | Partial { - msg = resolveJsonValues(msg); - if (msg.appearance) { - msg.appearance = Appearance.sanitize(msg.appearance); - } - return msg; - } - - /** @hidden */ - public actorChanged = (...path: string[]) => { - if (this.internal.observing) { - this.internal.patch = this.internal.patch || {} as ActorLike; - readPath(this, this.internal.patch, ...path); - this.context.internal.incrementGeneration(); - } - }; - - /** - * PRIVATE METHODS - */ - - private generateColliderGeometry( - colliderType: ColliderType, - size?: number | Vector3Like, - center = { x: 0, y: 0, z: 0 } as Vector3Like, - ): ColliderGeometry { - switch (colliderType) { - case ColliderType.Box: - return { - shape: ColliderType.Box, - center, - size: size as Readonly - }; - case ColliderType.Sphere: - return { - shape: ColliderType.Sphere, - center, - radius: size as number - }; - case ColliderType.Capsule: - return { - shape: ColliderType.Capsule, - center, - size: size as Readonly - }; - case 'auto': - return { - shape: ColliderType.Auto - }; - default: - log.error(null, - 'Trying to enable a collider on the actor with an invalid collider geometry type.' + - `Type given is ${colliderType}`); - - return undefined; - } - } - - private _setCollider(collider: Partial) { - const oldCollider = this._collider; - if (this._collider) { - unobserve(this._collider); - this._collider = undefined; - } - - this._collider = new Collider(this, collider); - if (oldCollider) { - this._collider.internal.copyHandlers(oldCollider.internal); - } - - // Actor patching: Observe the collider component for changed values. - observe({ - target: this._collider, - targetName: 'collider', - notifyChanged: (...path: string[]) => this.actorChanged(...path), - // Trigger notifications for every observed leaf node to ensure we get all values in the initial patch. - triggerNotificationsNow: true - }); - } -} +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import events from 'events'; +import { + ActionHandler, + ActionState, + ActorTransform, + ActorTransformLike, + Animation, + Appearance, + AppearanceLike, + Asset, + AssetContainer, + Attachment, + AttachmentLike, + AttachPoint, + Behavior, + Collider, + ColliderGeometry, + ColliderLike, + ColliderType, + CollisionLayer, + Context, + CreateAnimationOptions, + DiscreteAction, + Guid, + Light, + LightLike, + log, + LookAt, + LookAtLike, + LookAtMode, + MediaInstance, + Prefab, + PrimitiveDefinition, + ReadonlyMap, + RigidBody, + RigidBodyLike, + SetAnimationStateOptions, + SetAudioStateOptions, + SetVideoStateOptions, + Text, + TextLike, + User, + Vector3Like, + ZeroGuid, +} from '..'; +import { + observe, + Patchable, + readPath, + resolveJsonValues, + SubscriptionType, + unobserve +} from '../internal'; +import { ActorInternal } from './actorInternal'; + +/** + * Describes the properties of an Actor. + */ +export interface ActorLike { + id: Guid; + parentId: Guid; + name: string; + tag: string; + + /** + * When supplied, this actor will be unsynchronized, and only exist on the client + * of the User with the given ID. This value can only be set at actor creation. + * Any actors parented to this actor will also be exclusive to the given user. + */ + exclusiveToUser: Guid; + subscriptions: SubscriptionType[]; + transform: Partial; + appearance: Partial; + light: Partial; + rigidBody: Partial; + collider: Partial; + text: Partial; + attachment: Partial; + lookAt: Partial; + grabbable: boolean; +} + +/** + * An actor represents an object instantiated on the host. + */ +export class Actor implements ActorLike, Patchable { + private _internal = new ActorInternal(this); + /** @hidden */ + public get internal() { return this._internal; } + + private _emitter = new events.EventEmitter(); + /** @hidden */ + public get emitter() { return this._emitter; } + + private _name: string; + private _tag: string; + private _exclusiveToUser: Guid; + private _parentId = ZeroGuid; + private _subscriptions: SubscriptionType[] = []; + private _transform = new ActorTransform(); + private _appearance = new Appearance(this); + private _light: Light; + private _rigidBody: RigidBody; + private _collider: Collider; + private _text: Text; + private _attachment: Attachment; + private _lookAt: LookAt; + private _grabbable = false; + private _grab: DiscreteAction; + + private get grab() { this._grab = this._grab || new DiscreteAction(); return this._grab; } + + /* + * PUBLIC ACCESSORS + */ + + public get context() { return this._context; } + public get id() { return this._id; } + public get name() { return this._name; } + public get tag() { return this._tag; } + public set tag(value) { this._tag = value; this.actorChanged('tag'); } + + /** @inheritdoc */ + public get exclusiveToUser() { return this._exclusiveToUser; } + public get subscriptions() { return this._subscriptions; } + public get transform() { return this._transform; } + public set transform(value) { this._transform.copy(value); } + public get appearance() { return this._appearance; } + public set appearance(value) { this._appearance.copy(value); } + public get light() { return this._light; } + public get rigidBody() { return this._rigidBody; } + public get collider() { return this._collider; } + public get text() { return this._text; } + public get attachment() { return this._attachment; } + public get lookAt() { return this._lookAt; } + public get children() { return this.context.actors.filter(actor => actor.parentId === this.id); } + public get parent() { return this._context.actor(this._parentId); } + public set parent(value) { this.parentId = value && value.id || ZeroGuid; } + public get parentId() { return this._parentId; } + public set parentId(value) { + const parentActor = this.context.actor(value); + if (!value || !parentActor) { + value = ZeroGuid; + } + if (parentActor && parentActor.exclusiveToUser && parentActor.exclusiveToUser !== this.exclusiveToUser) { + throw new Error(`User-exclusive actor ${this.id} can only be parented to inclusive actors ` + + "and actors that are exclusive to the same user."); + } + if (this._parentId !== value) { + this._parentId = value; + this.actorChanged('parentId'); + } + } + + public get grabbable() { return this._grabbable; } + public set grabbable(value) { + if (value !== this._grabbable) { + this._grabbable = value; + this.actorChanged('grabbable'); + } + } + + private constructor(private _context: Context, private _id: Guid) { + // Actor patching: Observe the transform for changed values. + observe({ + target: this._transform, + targetName: 'transform', + notifyChanged: (...path: string[]) => this.actorChanged(...path) + }); + + // Observe changes to the looks of this actor + observe({ + target: this._appearance, + targetName: 'appearance', + notifyChanged: (...path: string[]) => this.actorChanged(...path) + }); + } + + /** + * @hidden + * TODO - get rid of this. + */ + public static alloc(context: Context, id: Guid): Actor { + return new Actor(context, id); + } + + /** + * PUBLIC METHODS + */ + + /** + * Creates a new, empty actor without geometry. + * @param context The SDK context object. + * @param options.actor The initial state of the actor. + */ + public static Create(context: Context, options?: { + actor?: Partial; + }): Actor { + return context.internal.Create(options); + } + + /** + * @deprecated + * Use [[Actor.Create]] instead. + */ + public static CreateEmpty(context: Context, options?: { + actor?: Partial; + }): Actor { + return Actor.Create(context, options); + } + + /** + * Creates a new actor from a library resource. + * Host-specific list of library resources. For AltspaceVR, see: https://account.altvr.com/kits + * @param context The SDK context object. + * @param options.resourceId The id of the library resource to instantiate. + * @param options.actor The initial state of the root actor. + */ + public static CreateFromLibrary(context: Context, options: { + resourceId: string; + actor?: Partial; + }): Actor { + return context.internal.CreateFromLibrary(options); + } + + /** + * Creates a new actor hierarchy from the provided prefab. + * @param context The SDK context object. + * @param options.prefabId The ID of a prefab asset to spawn. + * @param options.collisionLayer If the prefab contains colliders, put them on this layer. + * @param options.actor The initial state of the root actor. + */ + public static CreateFromPrefab(context: Context, options: { + prefabId: Guid; + collisionLayer?: CollisionLayer; + actor?: Partial; + }): Actor; + + /** + * Creates a new actor hierarchy from the provided prefab. + * @param context The SDK context object. + * @param options.prefab The prefab asset to spawn. + * @param options.collisionLayer If the prefab contains colliders, put them on this layer. + * @param options.actor The initial state of the root actor. + */ + public static CreateFromPrefab(context: Context, options: { + prefab: Prefab; + collisionLayer?: CollisionLayer; + actor?: Partial; + }): Actor; + + /** + * Creates a new actor hierarchy from the provided prefab. + * @param context The SDK context object. + * @param options.firstPrefabFrom An asset array containing at least one prefab. + * @param options.collisionLayer If the prefab contains colliders, put them on this layer. + * @param options.actor The initial state of the root actor. + */ + public static CreateFromPrefab(context: Context, options: { + firstPrefabFrom: Asset[]; + collisionLayer?: CollisionLayer; + actor?: Partial; + }): Actor; + + public static CreateFromPrefab(context: Context, options: { + prefabId?: Guid; + prefab?: Prefab; + firstPrefabFrom?: Asset[]; + collisionLayer?: CollisionLayer; + actor?: Partial; + }): Actor { + let prefabId = options.prefabId; + if (!prefabId && options.prefab) { + prefabId = options.prefab.id; + } + if (!prefabId && options.firstPrefabFrom) { + prefabId = options.firstPrefabFrom.find(a => !!a.prefab).id; + } + if (!prefabId) { + throw new Error("No prefab supplied to CreateFromPrefab"); + } + + return context.internal.CreateFromPrefab({ + prefabId, + collisionLayer: options.collisionLayer, + actor: options.actor + }); + } + + /** + * Load a glTF model, and spawn the first prefab in the resulting assets. Equivalent + * to using [[AssetContainer.loadGltf]] and [[Actor.CreateFromPrefab]]. + * @param container The asset container to load the glTF assets into + * @param options.uri A URI to a .gltf or .glb file + * @param options.colliderType The type of collider to add to each mesh actor + * @param options.actor The initial state of the actor + */ + public static CreateFromGltf(container: AssetContainer, options: { + uri: string; + colliderType?: 'box' | 'mesh'; + actor?: Partial; + }): Actor { + return container.context.internal.CreateFromGltf(container, options); + } + + /** + * Create an actor with a newly generated mesh. Equivalent to using + * [[AssetContainer.createPrimitiveMesh]] and adding the result to [[Actor.Create]]. + * @param container The asset container to load the mesh into + * @param options.definition The primitive shape and size + * @param options.addCollider Add an auto-typed collider to the actor + * @param options.actor The initial state of the actor + */ + public static CreatePrimitive(container: AssetContainer, options: { + definition: PrimitiveDefinition; + addCollider?: boolean; + actor?: Partial; + }): Actor { + const actor = options.actor || {}; + const mesh = container.createPrimitiveMesh(actor.name, options.definition); + return Actor.Create(container.context, { + actor: { + ...actor, + appearance: { + ...actor.appearance, + meshId: mesh.id + }, + collider: options.addCollider + ? actor.collider || { geometry: { shape: ColliderType.Auto } } + : actor.collider + } + }); + } + + /** + * Creates a Promise that will resolve once the actor is created on the host. + * @returns Promise + */ + public created(): Promise { + if (!this.internal.created) { + return new Promise((resolve, reject) => this.internal.enqueueCreatedPromise({ resolve, reject })); + } + if (this.internal.created.success) { + return Promise.resolve(); + } else { + return Promise.reject(this.internal.created.reason); + } + } + + /** + * Destroys the actor. + */ + public destroy(): void { + this.context.internal.destroyActor(this.id); + } + + /** + * Adds a light component to the actor. + * @param light Light characteristics. + */ + public enableLight(light?: Partial) { + if (!this._light) { + this._light = new Light(); + // Actor patching: Observe the light component for changed values. + observe({ + target: this._light, + targetName: 'light', + notifyChanged: (...path: string[]) => this.actorChanged(...path), + // Trigger notifications for every observed leaf node to ensure we get all values in the initial patch. + triggerNotificationsNow: true + }); + } + // Copying the new values will trigger an actor update and enable/update the light component. + this._light.copy(light); + } + + /** + * Adds a rigid body component to the actor. + * @param rigidBody Rigid body characteristics. + */ + public enableRigidBody(rigidBody?: Partial) { + if (!this._rigidBody) { + this._rigidBody = new RigidBody(this); + // Actor patching: Observe the rigid body component for changed values. + observe({ + target: this._rigidBody, + targetName: 'rigidBody', + notifyChanged: (...path: string[]) => this.actorChanged(...path), + // Trigger notifications for every observed leaf node to ensure we get all values in the initial patch. + triggerNotificationsNow: true + }); + } + // Copying the new values will trigger an actor update and enable/update the rigid body component. + this._rigidBody.copy(rigidBody); + } + + /** + * Adds a collider of the given type and parameters on the actor. + * @param colliderType Type of the collider to enable. + * @param isTrigger Whether the collider is a trigger volume or not. + * @param radius The radius of the collider. If omitted, a best-guess radius is chosen + * based on the size of the currently assigned mesh (loading meshes are not considered). + * If no mesh is assigned, defaults to 0.5. + * @param center The center of the collider, or default of the object if none is provided. + */ + // * @param collisionLayer The layer that the collider operates in. + public setCollider( + colliderType: ColliderType.Sphere, + // collisionLayer: CollisionLayer, + isTrigger: boolean, + radius?: number, + center?: Vector3Like + ): void; + + /** + * Adds a collider of the given type and parameters on the actor. + * @param colliderType Type of the collider to enable. + * @param isTrigger Whether the collider is a trigger volume or not. + * @param size The dimensions of the collider. If omitted, a best-guess size is chosen + * based on the currently assigned mesh (loading meshes are not considered). + * If no mesh is assigned, defaults to (1,1,1). + * @param center The center of the collider, or default of the object if none is provided. + */ + public setCollider( + colliderType: ColliderType.Box, + // collisionLayer: CollisionLayer, + isTrigger: boolean, + size?: Vector3Like, + center?: Vector3Like + ): void; + + /** + * Adds a collider of the give type and parameters on the actor. + * @param colliderType Type of the collider to enable. + * @param isTrigger Whether the collider is a trigger volume or not. + * @param size The dimensions of the collider, with the largest component of the vector + * being the primary axis and height of the capsule (including end caps), and the smallest the diameter. + * If omitted, a best-guess size is chosen based on the currently assigned mesh + * (loading meshes are not considered). If no mesh is assigned, defaults to (1, 1, 1). + * @param center The center of the collider, or default of the object if none is provided. + */ + public setCollider( + colliderType: ColliderType.Capsule, + isTrigger: boolean, + size?: Vector3Like, + center?: Vector3Like + ): void; + + /** + * Adds a collider whose shape is determined by the current mesh. + * @param colliderType Type of the collider to enable. + * @param isTrigger Whether the collider is a trigger volume or not. + */ + public setCollider( + colliderType: ColliderType.Auto, + isTrigger: boolean + ): void; + + public setCollider( + colliderType: ColliderType, + // collisionLayer: CollisionLayer, + isTrigger: boolean, + size?: number | Vector3Like, + center = { x: 0, y: 0, z: 0 } as Vector3Like + ): void { + const colliderGeometry = this.generateColliderGeometry(colliderType, size, center); + if (colliderGeometry) { + this._setCollider({ + enabled: true, + isTrigger, + // collisionLayer, + geometry: colliderGeometry + } as ColliderLike); + } + } + + /** + * Adds a text component to the actor. + * @param text Text characteristics + */ + public enableText(text?: Partial) { + if (!this._text) { + this._text = new Text(); + // Actor patching: Observe the text component for changed values. + observe({ + target: this._text, + targetName: 'text', + notifyChanged: (...path: string[]) => this.actorChanged(...path), + // Trigger notifications for every observed leaf node to ensure we get all values in the initial patch. + triggerNotificationsNow: true + }); + } + // Copying the new values will trigger an actor update and enable/update the text component. + this._text.copy(text); + } + + /** + * Instruct the actor to face another object, or stop facing an object. + * @param actorOrActorId The Actor or id of the actor to face. + * @param lookAtMode (Optional) How to face the target. @see LookUpMode. + * @param backward (Optional) If true, actor faces away from target rather than toward. + */ + public enableLookAt(actorOrActorId: Actor | Guid, mode?: LookAtMode, backward?: boolean) { + // Resolve the actorId value. + let actorId = ZeroGuid; + if (actorOrActorId instanceof Actor && actorOrActorId.id !== undefined) { + actorId = actorOrActorId.id; + } else if (typeof (actorOrActorId) === 'string') { + actorId = actorOrActorId; + } + // Allocate component if necessary. + if (!this._lookAt) { + this._lookAt = new LookAt(); + // Actor patching: Observe the lookAt component for changed values. + observe({ + target: this._lookAt, + targetName: 'lookAt', + notifyChanged: (...path: string[]) => this.actorChanged(...path), + // Trigger notifications for every observed leaf node to ensure we get all values in the + // initial patch. + triggerNotificationsNow: true + }); + } + // Set component values. + this._lookAt.copy({ + actorId, + mode, + backward + }); + } + + /** + * Attach to the user at the given attach point. + * @param userOrUserId The User or id of user to attach to. + * @param attachPoint Where on the user to attach. + */ + public attach(userOrUserId: User | Guid, attachPoint: AttachPoint) { + const userId = userOrUserId instanceof User ? userOrUserId.id : userOrUserId; + if (!this._attachment) { + // Actor patching: Observe the attachment for changed values. + this._attachment = new Attachment(); + observe({ + target: this._attachment, + targetName: 'attachment', + notifyChanged: (...path: string[]) => this.actorChanged(...path) + }); + } + this._attachment.userId = userId; + this._attachment.attachPoint = attachPoint; + } + + /** + * If attached to a user, detach from it. + */ + public detach() { + this._attachment.userId = ZeroGuid; + this._attachment.attachPoint = 'none'; + } + + /** + * Subscribe to updates from this actor. + * @param subscription The type of subscription to add. + */ + public subscribe(subscription: SubscriptionType) { + this._subscriptions.push(subscription); + this.actorChanged('subscriptions'); + } + + /** + * Unsubscribe from updates from this actor. + * @param subscription The type of subscription to remove. + */ + public unsubscribe(subscription: SubscriptionType) { + this._subscriptions = this._subscriptions.filter(value => value !== subscription); + this.actorChanged('subscriptions'); + } + + /** + * Add a grad handler to be called when the given action state has changed. + * @param grabState The grab state to fire the handler on. + * @param handler The handler to call when the grab state has changed. + */ + public onGrab(grabState: 'begin' | 'end', handler: ActionHandler) { + const actionState: ActionState = (grabState === 'begin') ? 'started' : 'stopped'; + this.grab.on(actionState, handler); + } + + /** + * Sets the behavior on this actor. + * @param behavior The type of behavior to set. Pass null to clear the behavior. + */ + public setBehavior(behavior: { new(): BehaviorT }): BehaviorT { + if (behavior) { + const newBehavior = new behavior(); + this.internal.behavior = newBehavior; + this.context.internal.setBehavior(this.id, this.internal.behavior.behaviorType); + return newBehavior; + } + + this.internal.behavior = null; + this.context.internal.setBehavior(this.id, null); + return null; + } + + /** + * Starts playing a preloaded sound. + * @param soundAssetId Name of sound asset preloaded using AssetManager. + * @param options Adjustments to pitch and volume, and other characteristics. + */ + public startSound( + soundAssetId: Guid, + options: SetAudioStateOptions, + ): MediaInstance { + return new MediaInstance(this, soundAssetId).start(options); + } + + /** + * Starts playing a preloaded video stream. + * @param videoStreamAssetId Name of video stream asset preloaded using AssetManager. + * @param options Adjustments to pitch and volume, and other characteristics. + */ + public startVideoStream( + videoStreamAssetId: Guid, + options: SetVideoStateOptions, + ): MediaInstance { + return new MediaInstance(this, videoStreamAssetId).start(options); + } + + /** + * Creates an animation on the actor. + * @param animationName The name of the animation. + * @param options The animation keyframes, events, and other characteristics. + * @returns A promise resolving to the resulting animation instance. + */ + public createAnimation(animationName: string, options: CreateAnimationOptions) { + return this.context.internal.createAnimation(this.id, animationName, options); + } + + /** + * @deprecated Set [[Animation.isPlaying]] instead. + * Enables the animation on the actor. Animation will start playing immediately. + * @param animationName The name of the animation. + */ + public enableAnimation(animationName: string) { + this.setAnimationState(animationName, { enabled: true }); + } + + /** + * @deprecated Set [[Animation.isPlaying]] instead. + * Disables the animation on the actor. Animation will stop playing immediately. + * When an animation is disabled, it is also paused (its time does not move forward). + * @param animationName The name of the animation. + */ + public disableAnimation(animationName: string) { + this.setAnimationState(animationName, { enabled: false }); + } + + /** + * @deprecated Set [[Animation.isPlaying]] instead. + * Starts the animation (sets animation speed to 1). + * @param animationName The name of the animation. + */ + public resumeAnimation(animationName: string) { + this.setAnimationState(animationName, { enabled: true }); + } + + /** + * @deprecated Set [[Animation.isPlaying]] instead. + * Stops the animation (sets animation speed to zero). + * @param animationName The name of the animation. + */ + public pauseAnimation(animationName: string) { + this.setAnimationState(animationName, { enabled: false }); + } + + /** + * @deprecated Set [[Animation.time]] instead. + * Sets the animation time (units are in seconds). + * @param animationName The name of the animation. + * @param time The desired animation time. A negative value seeks to the end of the animation. + */ + public setAnimationTime(animationName: string, time: number) { + this.setAnimationState(animationName, { time }); + } + + /** + * @deprecated Set properties of an [[Animation]] instance instead. + * (Advanced) Sets the time, speed, and enabled state of an animation. + * @param animationName The name of the animation. + * @param options The time, speed and enabled state to apply. All values are optional. Only the values + * provided will be applied. + */ + public setAnimationState(animationName: string, state: SetAnimationStateOptions) { + return this.context.internal.setAnimationState(this.id, animationName, state); + } + + /** + * Animate actor properties to the given value, following the specified animation curve. Actor transform + * is the only animatable property at the moment. Other properties such as light color may become animatable + * in the future. + * @param value The desired final state of the animation. + * @param duration The length of the interpolation (in seconds). + * @param curve The cubic-bezier curve parameters. @see AnimationEaseCurves for predefined values. + */ + public animateTo(value: Partial, duration: number, curve: number[]) { + this.context.internal.animateTo(this.id, value, duration, curve); + } + + /** + * Finds child actors matching `name`. + * @param name The name of the actors to find. + * @param recurse Whether or not to search recursively. + */ + public findChildrenByName(name: string, recurse: boolean): Actor[] { + const namedChildren = this.children.filter(actor => actor.name === name); + if (!recurse) { + return namedChildren; + } + + for (const child of this.children) { + namedChildren.push(...child.findChildrenByName(name, recurse)); + } + + return namedChildren; + } + + /** + * Actor Events + */ + + /** + * Set an event handler for the animation-disabled event. + * @param handler The handler to call when an animation reaches the end or is otherwise disabled. + */ + public onAnimationDisabled(handler: (animationName: string) => any): this { + this.emitter.addListener('animation-disabled', handler); + return this; + } + + /** + * Set an event handler for the animation-enabled event. + * @param handler The handler to call when an animation moves from the disabled to enabled state. + */ + public onAnimationEnabled(handler: (animationName: string) => any): this { + this.emitter.addListener('animation-enabled', handler); + return this; + } + + /** The list of animations that target this actor, by ID. */ + public get animations() { + return [...this.context.internal.animationSet.values()] + .filter(anim => anim.targetActorIds.includes(this.id)) + .reduce( + (map, anim) => { + map.set(anim.id, anim); + return map; + }, + new Map() + ) as ReadonlyMap; + } + + /** The list of animations that target this actor, by name. */ + public get animationsByName() { + return [...this.context.internal.animationSet.values()] + .filter(anim => anim.targetActorIds.includes(this.id) && anim.name) + .reduce( + (map, anim) => { + map.set(anim.name, anim); + return map; + }, + new Map() + ) as ReadonlyMap; + } + + /** Recursively search for the named animation from this actor. */ + public findAnimationInChildrenByName(name: string): Animation { + if (this.animationsByName.has(name)) { + return this.animationsByName.get(name); + } else { + return this.children.reduce( + (val, child) => val || child.findAnimationInChildrenByName(name), + null as Animation + ); + } + } + + /** @hidden */ + public copy(from: Partial): this { + // Pause change detection while we copy the values into the actor. + const wasObserving = this.internal.observing; + this.internal.observing = false; + + if (!from) { return this; } + if (from.id) { this._id = from.id; } + if (from.parentId) { this._parentId = from.parentId; } + if (from.name) { this._name = from.name; } + if (from.tag) { this._tag = from.tag; } + if (from.exclusiveToUser || from.parentId) { + this._exclusiveToUser = this.parent && this.parent.exclusiveToUser || from.exclusiveToUser; + } + if (from.transform) { this._transform.copy(from.transform); } + if (from.attachment) { this.attach(from.attachment.userId, from.attachment.attachPoint); } + if (from.appearance) { this._appearance.copy(from.appearance); } + if (from.light) { this.enableLight(from.light); } + if (from.rigidBody) { this.enableRigidBody(from.rigidBody); } + if (from.collider) { this._setCollider(from.collider); } + if (from.text) { this.enableText(from.text); } + if (from.lookAt) { this.enableLookAt(from.lookAt.actorId, from.lookAt.mode); } + if (from.grabbable !== undefined) { this._grabbable = from.grabbable; } + + this.internal.observing = wasObserving; + return this; + } + + /** @hidden */ + public toJSON() { + return { + id: this._id, + parentId: this._parentId, + name: this._name, + tag: this._tag, + exclusiveToUser: this._exclusiveToUser, + transform: this._transform.toJSON(), + appearance: this._appearance.toJSON(), + attachment: this._attachment ? this._attachment.toJSON() : undefined, + light: this._light ? this._light.toJSON() : undefined, + rigidBody: this._rigidBody ? this._rigidBody.toJSON() : undefined, + collider: this._collider ? this._collider.toJSON() : undefined, + text: this._text ? this._text.toJSON() : undefined, + lookAt: this._lookAt ? this._lookAt.toJSON() : undefined, + grabbable: this._grabbable + } as ActorLike; + } + + /** + * INTERNAL METHODS + */ + + /** + * Prepare outgoing messages + * @hidden + */ + public static sanitize(msg: ActorLike): ActorLike; + public static sanitize(msg: Partial): Partial; + public static sanitize(msg: ActorLike | Partial): ActorLike | Partial { + msg = resolveJsonValues(msg); + if (msg.appearance) { + msg.appearance = Appearance.sanitize(msg.appearance); + } + return msg; + } + + /** @hidden */ + public actorChanged = (...path: string[]) => { + if (this.internal.observing) { + this.internal.patch = this.internal.patch || {} as ActorLike; + readPath(this, this.internal.patch, ...path); + this.context.internal.incrementGeneration(); + } + }; + + /** + * PRIVATE METHODS + */ + + private generateColliderGeometry( + colliderType: ColliderType, + size?: number | Vector3Like, + center = { x: 0, y: 0, z: 0 } as Vector3Like, + ): ColliderGeometry { + switch (colliderType) { + case ColliderType.Box: + return { + shape: ColliderType.Box, + center, + size: size as Readonly + }; + case ColliderType.Sphere: + return { + shape: ColliderType.Sphere, + center, + radius: size as number + }; + case ColliderType.Capsule: + return { + shape: ColliderType.Capsule, + center, + size: size as Readonly + }; + case 'auto': + return { + shape: ColliderType.Auto + }; + default: + log.error(null, + 'Trying to enable a collider on the actor with an invalid collider geometry type.' + + `Type given is ${colliderType}`); + + return undefined; + } + } + + private _setCollider(collider: Partial) { + const oldCollider = this._collider; + if (this._collider) { + unobserve(this._collider); + this._collider = undefined; + } + + this._collider = new Collider(this, collider); + if (oldCollider) { + this._collider.internal.copyHandlers(oldCollider.internal); + } + + // Actor patching: Observe the collider component for changed values. + observe({ + target: this._collider, + targetName: 'collider', + notifyChanged: (...path: string[]) => this.actorChanged(...path), + // Trigger notifications for every observed leaf node to ensure we get all values in the initial patch. + triggerNotificationsNow: true + }); + } +} diff --git a/packages/sdk/src/types/internal/actor.ts b/packages/sdk/src/actor/actorInternal.ts similarity index 86% rename from packages/sdk/src/types/internal/actor.ts rename to packages/sdk/src/actor/actorInternal.ts index 4c1fb10d4..5806f5573 100644 --- a/packages/sdk/src/types/internal/actor.ts +++ b/packages/sdk/src/actor/actorInternal.ts @@ -1,103 +1,105 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -import { - ActionEvent, - Actor, - ActorLike, - Behavior, - CollisionData, - CollisionEventType, - DiscreteAction, - SetAnimationStateOptions, - TriggerEventType -} from '../..'; -import { ExportedPromise } from '../../utils/exportedPromise'; -import { InternalPatchable } from '../patchable'; -import { InternalCollider } from './collider'; - -/** - * @hidden - */ -export class InternalActor implements InternalPatchable { - public observing = true; - public patch: ActorLike; - public behavior: Behavior; - public createdPromises: ExportedPromise[]; - public created: { success: boolean; reason?: any }; - - public get collider(): InternalCollider { - return this.actor.collider ? this.actor.collider.internal : undefined; - } - - constructor(public actor: Actor) { - } - - public performAction(actionEvent: ActionEvent) { - const behavior = (this.behavior && this.behavior.behaviorType === actionEvent.behaviorType) - ? this.behavior : undefined; - if (behavior && behavior._supportsAction(actionEvent.actionName)) { - behavior._performAction(actionEvent.actionName, actionEvent.actionState, actionEvent.user); - } else { - const action = (this.actor as any)[actionEvent.actionName.toLowerCase()] as DiscreteAction; - if (action) { - action._setState(actionEvent.user, actionEvent.actionState); - } - } - } - - public collisionEventRaised(collisionEventType: CollisionEventType, collisionData: CollisionData) { - if (this.collider) { - this.collider.eventReceived(collisionEventType, collisionData); - } - } - - public triggerEventRaised(triggerEventType: TriggerEventType, otherActor: Actor) { - if (this.collider) { - this.collider.eventReceived(triggerEventType, otherActor); - } - } - - public setAnimationStateEventRaised(animationName: string, state: SetAnimationStateOptions) { - if (this.actor) { - if (state.enabled !== undefined) { - if (state.enabled) { - this.actor.emitter.emit('animation-enabled', animationName); - } else { - this.actor.emitter.emit('animation-disabled', animationName); - } - } - } - } - - public getPatchAndReset(): ActorLike { - const patch = this.patch; - if (patch) { - patch.id = this.actor.id; - delete this.patch; - return Actor.sanitize(patch); - } - } - - public notifyCreated(success: boolean, reason?: any): void { - this.created = { success, reason }; - if (this.createdPromises) { - const createdPromises = this.createdPromises; - delete this.createdPromises; - for (const promise of createdPromises) { - if (success) { - promise.resolve(); - } else { - promise.reject(reason); - } - } - } - } - - public enqueueCreatedPromise(promise: ExportedPromise): void { - this.createdPromises = this.createdPromises || []; - this.createdPromises.push(promise); - } -} +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { + ActionEvent, + Actor, + ActorLike, + Behavior, + CollisionData, + CollisionEventType, + DiscreteAction, + SetAnimationStateOptions, + TriggerEventType +} from '..'; +import { + ExportedPromise, + InternalPatchable +} from '../internal'; +import { ColliderInternal } from './physics/colliderInternal'; + +/** + * @hidden + */ +export class ActorInternal implements InternalPatchable { + public observing = true; + public patch: ActorLike; + public behavior: Behavior; + public createdPromises: ExportedPromise[]; + public created: { success: boolean; reason?: any }; + + public get collider(): ColliderInternal { + return this.actor.collider ? this.actor.collider.internal : undefined; + } + + constructor(public actor: Actor) { + } + + public performAction(actionEvent: ActionEvent) { + const behavior = (this.behavior && this.behavior.behaviorType === actionEvent.behaviorType) + ? this.behavior : undefined; + if (behavior && behavior._supportsAction(actionEvent.actionName)) { + behavior._performAction(actionEvent.actionName, actionEvent.actionState, actionEvent.user); + } else { + const action = (this.actor as any)[actionEvent.actionName.toLowerCase()] as DiscreteAction; + if (action) { + action._setState(actionEvent.user, actionEvent.actionState); + } + } + } + + public collisionEventRaised(collisionEventType: CollisionEventType, collisionData: CollisionData) { + if (this.collider) { + this.collider.eventReceived(collisionEventType, collisionData); + } + } + + public triggerEventRaised(triggerEventType: TriggerEventType, otherActor: Actor) { + if (this.collider) { + this.collider.eventReceived(triggerEventType, otherActor); + } + } + + public setAnimationStateEventRaised(animationName: string, state: SetAnimationStateOptions) { + if (this.actor) { + if (state.enabled !== undefined) { + if (state.enabled) { + this.actor.emitter.emit('animation-enabled', animationName); + } else { + this.actor.emitter.emit('animation-disabled', animationName); + } + } + } + } + + public getPatchAndReset(): ActorLike { + const patch = this.patch; + if (patch) { + patch.id = this.actor.id; + delete this.patch; + return Actor.sanitize(patch); + } + } + + public notifyCreated(success: boolean, reason?: any): void { + this.created = { success, reason }; + if (this.createdPromises) { + const createdPromises = this.createdPromises; + delete this.createdPromises; + for (const promise of createdPromises) { + if (success) { + promise.resolve(); + } else { + promise.reject(reason); + } + } + } + } + + public enqueueCreatedPromise(promise: ExportedPromise): void { + this.createdPromises = this.createdPromises || []; + this.createdPromises.push(promise); + } +} diff --git a/packages/sdk/src/types/runtime/actorTransform.ts b/packages/sdk/src/actor/actorTransform.ts similarity index 97% rename from packages/sdk/src/types/runtime/actorTransform.ts rename to packages/sdk/src/actor/actorTransform.ts index 3ac69240c..26893f796 100644 --- a/packages/sdk/src/types/runtime/actorTransform.ts +++ b/packages/sdk/src/actor/actorTransform.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. */ -import { ScaledTransform, ScaledTransformLike, Transform, TransformLike } from "./transform"; +import { ScaledTransform, ScaledTransformLike, Transform, TransformLike } from ".."; export interface ActorTransformLike { app: Partial; diff --git a/packages/sdk/src/types/runtime/appearance.ts b/packages/sdk/src/actor/appearance.ts similarity index 98% rename from packages/sdk/src/types/runtime/appearance.ts rename to packages/sdk/src/actor/appearance.ts index 477f45394..fd05d31d6 100644 --- a/packages/sdk/src/types/runtime/appearance.ts +++ b/packages/sdk/src/actor/appearance.ts @@ -3,8 +3,7 @@ * Licensed under the MIT License. */ -import { Actor, GroupMask } from '.'; -import { Guid, ZeroGuid } from '../..'; +import { Actor, GroupMask, Guid, ZeroGuid } from '..'; export interface AppearanceLike { /** diff --git a/packages/sdk/src/types/runtime/attachment.ts b/packages/sdk/src/actor/attachment.ts similarity index 93% rename from packages/sdk/src/types/runtime/attachment.ts rename to packages/sdk/src/actor/attachment.ts index 40a9714f9..87a63e669 100644 --- a/packages/sdk/src/types/runtime/attachment.ts +++ b/packages/sdk/src/actor/attachment.ts @@ -1,85 +1,85 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -import { Guid, ZeroGuid } from "../.."; - -/** - * The complete set of attach points. - */ -export type AttachPoint - = 'none' - | 'camera' - | 'head' - | 'neck' - | 'hips' - | 'center-eye' - | 'spine-top' - | 'spine-middle' - | 'spine-bottom' - | 'left-eye' - | 'left-upper-leg' - | 'left-lower-leg' - | 'left-foot' - | 'left-toes' - | 'left-shoulder' - | 'left-upper-arm' - | 'left-lower-arm' - | 'left-hand' - | 'left-thumb' - | 'left-index' - | 'left-middle' - | 'left-ring' - | 'left-pinky' - | 'right-eye' - | 'right-upper-leg' - | 'right-lower-leg' - | 'right-foot' - | 'right-toes' - | 'right-shoulder' - | 'right-upper-arm' - | 'right-lower-arm' - | 'right-hand' - | 'right-thumb' - | 'right-index' - | 'right-middle' - | 'right-ring' - | 'right-pinky' - ; - -/** - * The characteristics of an active attachment. - */ -export interface AttachmentLike { - userId: Guid; - attachPoint: AttachPoint; -} - -/** - * Implementation of AttachmentLike. This class is observable. - */ -export class Attachment implements AttachmentLike { - private _userId = ZeroGuid; - private _attachPoint: AttachPoint = 'none'; - - public get userId() { return this._userId; } - public set userId(value) { this._userId = value || ZeroGuid; } - public get attachPoint() { return this._attachPoint; } - public set attachPoint(value) { this._attachPoint = value || 'none'; } - - /** @hidden */ - public toJSON(): AttachmentLike { - return { - userId: this.userId, - attachPoint: this.attachPoint - } as AttachmentLike; - } - - public copy(from: Partial): this { - if (!from) { return this; } - if (from.userId) { this._userId = from.userId; } - if (from.attachPoint) { this._attachPoint = from.attachPoint; } - return this; - } -} +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { Guid, ZeroGuid } from ".."; + +/** + * The complete set of attach points. + */ +export type AttachPoint + = 'none' + | 'camera' + | 'head' + | 'neck' + | 'hips' + | 'center-eye' + | 'spine-top' + | 'spine-middle' + | 'spine-bottom' + | 'left-eye' + | 'left-upper-leg' + | 'left-lower-leg' + | 'left-foot' + | 'left-toes' + | 'left-shoulder' + | 'left-upper-arm' + | 'left-lower-arm' + | 'left-hand' + | 'left-thumb' + | 'left-index' + | 'left-middle' + | 'left-ring' + | 'left-pinky' + | 'right-eye' + | 'right-upper-leg' + | 'right-lower-leg' + | 'right-foot' + | 'right-toes' + | 'right-shoulder' + | 'right-upper-arm' + | 'right-lower-arm' + | 'right-hand' + | 'right-thumb' + | 'right-index' + | 'right-middle' + | 'right-ring' + | 'right-pinky' + ; + +/** + * The characteristics of an active attachment. + */ +export interface AttachmentLike { + userId: Guid; + attachPoint: AttachPoint; +} + +/** + * Implementation of AttachmentLike. This class is observable. + */ +export class Attachment implements AttachmentLike { + private _userId = ZeroGuid; + private _attachPoint: AttachPoint = 'none'; + + public get userId() { return this._userId; } + public set userId(value) { this._userId = value || ZeroGuid; } + public get attachPoint() { return this._attachPoint; } + public set attachPoint(value) { this._attachPoint = value || 'none'; } + + /** @hidden */ + public toJSON(): AttachmentLike { + return { + userId: this.userId, + attachPoint: this.attachPoint + } as AttachmentLike; + } + + public copy(from: Partial): this { + if (!from) { return this; } + if (from.userId) { this._userId = from.userId; } + if (from.attachPoint) { this._attachPoint = from.attachPoint; } + return this; + } +} diff --git a/packages/sdk/src/types/runtime/behaviors/action.ts b/packages/sdk/src/actor/behaviors/action.ts similarity index 93% rename from packages/sdk/src/types/runtime/behaviors/action.ts rename to packages/sdk/src/actor/behaviors/action.ts index bd2d7fd42..f72d01f22 100644 --- a/packages/sdk/src/types/runtime/behaviors/action.ts +++ b/packages/sdk/src/actor/behaviors/action.ts @@ -1,87 +1,86 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -import { ActionState } from '.'; -import { Guid, User } from '../../..'; - -/** - * The action handler function type. - */ -export type ActionHandler = (user: User) => void; - -interface ActionHandlers { - 'started'?: ActionHandler; - 'stopped'?: ActionHandler; -} - -/** - * Class that represents a discrete action that can be in one of two states, - * started or stopped for each user. @see ActionState - */ -export class DiscreteAction { - private handlers: ActionHandlers = {}; - private activeUserIds: Guid[] = []; - - /** - * Add a handler for the given action state for when it is triggered. - * @param actionState The action state that the handle should be assigned to. - * @param handler The handler to call when the action state is triggered. - */ - public on(actionState: ActionState, handler: ActionHandler): this { - this.handlers[actionState] = handler; - return this; - } - - /** - * Gets the current state of the action for the user with the given id. - * @param user The user to get the action state for. - * @returns The current state of the action for the user. - */ - public getState(user: User): ActionState { - return this.activeUserIds.find(id => id === user.id) ? - 'started' : 'stopped'; - } - - /** - * Get whether the action is active for the user with the given id. - * @param user - The user to get whether the action is active for, or null - * if active for any user is desired.. - * @returns - True if the action is active for the user, false if it is not. In the case - * that no user is given, the value is true if the action is active for any user, and false - * if not. - */ - public isActive(user?: User): boolean { - if (user) { - return !!this.activeUserIds.find(id => id === user.id); - } else { - return this.activeUserIds.length > 0; - } - } - - /** - * INTERNAL METHODS - */ - - /** @hidden */ - public _setState(user: User, actionState: ActionState): boolean { - const currentState = this.activeUserIds.find(id => id === user.id) || 'stopped'; - if (currentState !== actionState) { - if (actionState === 'started') { - this.activeUserIds.push(user.id); - } else { - this.activeUserIds = this.activeUserIds.filter(id => id === user.id); - } - - const handler = this.handlers[actionState]; - if (handler) { - handler(user); - } - - return true; - } - - return false; - } -} +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { ActionState, Guid, User } from '../..'; + +/** + * The action handler function type. + */ +export type ActionHandler = (user: User) => void; + +interface ActionHandlers { + 'started'?: ActionHandler; + 'stopped'?: ActionHandler; +} + +/** + * Class that represents a discrete action that can be in one of two states, + * started or stopped for each user. @see ActionState + */ +export class DiscreteAction { + private handlers: ActionHandlers = {}; + private activeUserIds: Guid[] = []; + + /** + * Add a handler for the given action state for when it is triggered. + * @param actionState The action state that the handle should be assigned to. + * @param handler The handler to call when the action state is triggered. + */ + public on(actionState: ActionState, handler: ActionHandler): this { + this.handlers[actionState] = handler; + return this; + } + + /** + * Gets the current state of the action for the user with the given id. + * @param user The user to get the action state for. + * @returns The current state of the action for the user. + */ + public getState(user: User): ActionState { + return this.activeUserIds.find(id => id === user.id) ? + 'started' : 'stopped'; + } + + /** + * Get whether the action is active for the user with the given id. + * @param user - The user to get whether the action is active for, or null + * if active for any user is desired.. + * @returns - True if the action is active for the user, false if it is not. In the case + * that no user is given, the value is true if the action is active for any user, and false + * if not. + */ + public isActive(user?: User): boolean { + if (user) { + return !!this.activeUserIds.find(id => id === user.id); + } else { + return this.activeUserIds.length > 0; + } + } + + /** + * INTERNAL METHODS + */ + + /** @hidden */ + public _setState(user: User, actionState: ActionState): boolean { + const currentState = this.activeUserIds.find(id => id === user.id) || 'stopped'; + if (currentState !== actionState) { + if (actionState === 'started') { + this.activeUserIds.push(user.id); + } else { + this.activeUserIds = this.activeUserIds.filter(id => id === user.id); + } + + const handler = this.handlers[actionState]; + if (handler) { + handler(user); + } + + return true; + } + + return false; + } +} diff --git a/packages/sdk/src/types/runtime/behaviors/actionEvent.ts b/packages/sdk/src/actor/behaviors/actionEvent.ts similarity index 66% rename from packages/sdk/src/types/runtime/behaviors/actionEvent.ts rename to packages/sdk/src/actor/behaviors/actionEvent.ts index 0243a7d59..697c81bce 100644 --- a/packages/sdk/src/types/runtime/behaviors/actionEvent.ts +++ b/packages/sdk/src/actor/behaviors/actionEvent.ts @@ -1,16 +1,14 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -import { ActionState, BehaviorType } from '.'; -import { User } from '..'; -import { Guid } from '../../..'; - -export interface ActionEvent { - user: User; - targetId: Guid; - behaviorType: BehaviorType; - actionName: string; - actionState: ActionState; -} +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { ActionState, BehaviorType, Guid, User } from '../..'; + +export interface ActionEvent { + user: User; + targetId: Guid; + behaviorType: BehaviorType; + actionName: string; + actionState: ActionState; +} diff --git a/packages/sdk/src/types/runtime/behaviors/actionStates.ts b/packages/sdk/src/actor/behaviors/actionStates.ts similarity index 94% rename from packages/sdk/src/types/runtime/behaviors/actionStates.ts rename to packages/sdk/src/actor/behaviors/actionStates.ts index 616e3727c..698e25ddf 100644 --- a/packages/sdk/src/types/runtime/behaviors/actionStates.ts +++ b/packages/sdk/src/actor/behaviors/actionStates.ts @@ -1,9 +1,9 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -export type ActionState - = 'started' - | 'stopped' - ; +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +export type ActionState + = 'started' + | 'stopped' + ; diff --git a/packages/sdk/src/types/runtime/behaviors/behavior.ts b/packages/sdk/src/actor/behaviors/behavior.ts similarity index 86% rename from packages/sdk/src/types/runtime/behaviors/behavior.ts rename to packages/sdk/src/actor/behaviors/behavior.ts index dd6ad190d..8eb3050de 100644 --- a/packages/sdk/src/types/runtime/behaviors/behavior.ts +++ b/packages/sdk/src/actor/behaviors/behavior.ts @@ -1,34 +1,33 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -import { ActionState, BehaviorType, DiscreteAction } from '.'; -import { User } from '..'; - -/** - * Abstract class that serves as the base class for all behaviors. - */ -export abstract class Behavior { - /** - * Gets the readonly behavior type for this behavior. - */ - public abstract get behaviorType(): BehaviorType; - - /** - * INTERNAL METHODS - */ - - public _supportsAction(actionName: string): boolean { - const action = (this as any)[actionName.toLowerCase()] as DiscreteAction; - return action !== undefined; - } - - /** @hidden */ - public _performAction(actionName: string, actionState: ActionState, user: User): void { - const action = (this as any)[actionName.toLowerCase()] as DiscreteAction; - if (action) { - action._setState(user, actionState); - } - } -} +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { ActionState, BehaviorType, DiscreteAction, User } from '../..'; + +/** + * Abstract class that serves as the base class for all behaviors. + */ +export abstract class Behavior { + /** + * Gets the readonly behavior type for this behavior. + */ + public abstract get behaviorType(): BehaviorType; + + /** + * INTERNAL METHODS + */ + + public _supportsAction(actionName: string): boolean { + const action = (this as any)[actionName.toLowerCase()] as DiscreteAction; + return action !== undefined; + } + + /** @hidden */ + public _performAction(actionName: string, actionState: ActionState, user: User): void { + const action = (this as any)[actionName.toLowerCase()] as DiscreteAction; + if (action) { + action._setState(user, actionState); + } + } +} diff --git a/packages/sdk/src/types/runtime/behaviors/behaviorTypes.ts b/packages/sdk/src/actor/behaviors/behaviorTypes.ts similarity index 94% rename from packages/sdk/src/types/runtime/behaviors/behaviorTypes.ts rename to packages/sdk/src/actor/behaviors/behaviorTypes.ts index 07d3504da..04cb1ceb9 100644 --- a/packages/sdk/src/types/runtime/behaviors/behaviorTypes.ts +++ b/packages/sdk/src/actor/behaviors/behaviorTypes.ts @@ -1,14 +1,14 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -/** - * These are the behavior types available to set on an actor. - */ - -export type BehaviorType - = 'none' - | 'target' - | 'button' - ; +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +/** + * These are the behavior types available to set on an actor. + */ + +export type BehaviorType + = 'none' + | 'target' + | 'button' + ; diff --git a/packages/sdk/src/types/runtime/behaviors/buttonBehavior.ts b/packages/sdk/src/actor/behaviors/buttonBehavior.ts similarity index 93% rename from packages/sdk/src/types/runtime/behaviors/buttonBehavior.ts rename to packages/sdk/src/actor/behaviors/buttonBehavior.ts index 04a88789b..2a7a8a552 100644 --- a/packages/sdk/src/types/runtime/behaviors/buttonBehavior.ts +++ b/packages/sdk/src/actor/behaviors/buttonBehavior.ts @@ -1,77 +1,83 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -import { ActionHandler, ActionState, BehaviorType, DiscreteAction, TargetBehavior } from '.'; -import { User } from '..'; - -/** - * Button behavior class containing the target behavior actions. - */ -export class ButtonBehavior extends TargetBehavior { - private _hover: DiscreteAction = new DiscreteAction(); - private _click: DiscreteAction = new DiscreteAction(); - private _button: DiscreteAction = new DiscreteAction(); - - /** @inheritdoc */ - public get behaviorType(): BehaviorType { return 'button'; } - - public get hover() { return this._hover; } - public get click() { return this._click; } - public get button() { return this._button; } - - /** - * Add a hover handler to be called when the given hover state is triggered. - * @param hoverState The hover state to fire the handler on. - * @param handler The handler to call when the hover state is triggered. - * @return This button behavior. - */ - public onHover(hoverState: 'enter' | 'exit', handler: ActionHandler): this { - const actionState: ActionState = (hoverState === 'enter') ? 'started' : 'stopped'; - this._hover.on(actionState, handler); - return this; - } - - /** - * Add a click handler to be called when the given click state is triggered. - * @param handler The handler to call when the click state is triggered. - * @return This button behavior. - */ - public onClick(handler: ActionHandler): this { - this._click.on('started', handler); - return this; - } - - /** - * Add a button handler to be called when a complete button click has occured. - * @param buttonState The button state to fire the handler on. - * @param handler The handler to call when the click state is triggered. - * @return This button behavior. - */ - public onButton(buttonState: 'pressed' | 'released', handler: ActionHandler): this { - const actionState: ActionState = (buttonState === 'pressed') ? 'started' : 'stopped'; - this._button.on(actionState, handler); - return this; - } - - /** - * Gets whether the button is being hovered over by the given user, or at all if no user id is given. - * @param user The user to check whether they are hovering over this button behavior. - * @return True if the user is hovering over, false if not. In the case where no user id is given, this - * returns true if any user is hovering over, false if none are. - */ - public isHoveredOver(user?: User): boolean { - return this._hover.isActive(user); - } - - /** - * Gets whether the button is being clicked by the given user, or at all if no user id is given. - * @param user The user to check whether they are clicking this button behavior. - * @return True if the user is clicking, false if not. In the case where no user id is given, this - * returns true if any user is clicking, false if none are. - */ - public isClicked(user?: User): boolean { - return this._click.isActive(user); - } -} +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { + ActionHandler, + ActionState, + BehaviorType, + DiscreteAction, + TargetBehavior, + User +} from '../..'; + +/** + * Button behavior class containing the target behavior actions. + */ +export class ButtonBehavior extends TargetBehavior { + private _hover: DiscreteAction = new DiscreteAction(); + private _click: DiscreteAction = new DiscreteAction(); + private _button: DiscreteAction = new DiscreteAction(); + + /** @inheritdoc */ + public get behaviorType(): BehaviorType { return 'button'; } + + public get hover() { return this._hover; } + public get click() { return this._click; } + public get button() { return this._button; } + + /** + * Add a hover handler to be called when the given hover state is triggered. + * @param hoverState The hover state to fire the handler on. + * @param handler The handler to call when the hover state is triggered. + * @return This button behavior. + */ + public onHover(hoverState: 'enter' | 'exit', handler: ActionHandler): this { + const actionState: ActionState = (hoverState === 'enter') ? 'started' : 'stopped'; + this._hover.on(actionState, handler); + return this; + } + + /** + * Add a click handler to be called when the given click state is triggered. + * @param handler The handler to call when the click state is triggered. + * @return This button behavior. + */ + public onClick(handler: ActionHandler): this { + this._click.on('started', handler); + return this; + } + + /** + * Add a button handler to be called when a complete button click has occured. + * @param buttonState The button state to fire the handler on. + * @param handler The handler to call when the click state is triggered. + * @return This button behavior. + */ + public onButton(buttonState: 'pressed' | 'released', handler: ActionHandler): this { + const actionState: ActionState = (buttonState === 'pressed') ? 'started' : 'stopped'; + this._button.on(actionState, handler); + return this; + } + + /** + * Gets whether the button is being hovered over by the given user, or at all if no user id is given. + * @param user The user to check whether they are hovering over this button behavior. + * @return True if the user is hovering over, false if not. In the case where no user id is given, this + * returns true if any user is hovering over, false if none are. + */ + public isHoveredOver(user?: User): boolean { + return this._hover.isActive(user); + } + + /** + * Gets whether the button is being clicked by the given user, or at all if no user id is given. + * @param user The user to check whether they are clicking this button behavior. + * @return True if the user is clicking, false if not. In the case where no user id is given, this + * returns true if any user is clicking, false if none are. + */ + public isClicked(user?: User): boolean { + return this._click.isActive(user); + } +} diff --git a/packages/sdk/src/types/runtime/behaviors/index.ts b/packages/sdk/src/actor/behaviors/index.ts similarity index 96% rename from packages/sdk/src/types/runtime/behaviors/index.ts rename to packages/sdk/src/actor/behaviors/index.ts index a861751dc..ce677d1b4 100644 --- a/packages/sdk/src/types/runtime/behaviors/index.ts +++ b/packages/sdk/src/actor/behaviors/index.ts @@ -1,12 +1,12 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -export * from './action'; -export * from './actionEvent'; -export * from './actionStates'; -export * from './behavior'; -export * from './behaviorTypes'; -export * from './targetBehavior'; -export * from './buttonBehavior'; +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +export * from './action'; +export * from './actionEvent'; +export * from './actionStates'; +export * from './behavior'; +export * from './behaviorTypes'; +export * from './buttonBehavior'; +export * from './targetBehavior'; diff --git a/packages/sdk/src/types/runtime/behaviors/targetBehavior.ts b/packages/sdk/src/actor/behaviors/targetBehavior.ts similarity index 89% rename from packages/sdk/src/types/runtime/behaviors/targetBehavior.ts rename to packages/sdk/src/actor/behaviors/targetBehavior.ts index 3a82766e0..724d5860b 100644 --- a/packages/sdk/src/types/runtime/behaviors/targetBehavior.ts +++ b/packages/sdk/src/actor/behaviors/targetBehavior.ts @@ -1,41 +1,47 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -import { ActionHandler, ActionState, Behavior, BehaviorType, DiscreteAction } from '.'; -import { User } from '..'; - -/** - * Target behavior class containing the target behavior actions. - */ -export class TargetBehavior extends Behavior { - private _target: DiscreteAction = new DiscreteAction(); - - /** @inheritdoc */ - public get behaviorType(): BehaviorType { return 'target'; } - - public get target() { return this._target; } - - /** - * Add a target handler to be called when the given target state is triggered. - * @param targetState The target state to fire the handler on. - * @param handler The handler to call when the target state is triggered. - * @return This target behavior. - */ - public onTarget(targetState: 'enter' | 'exit', handler: ActionHandler): this { - const actionState: ActionState = (targetState === 'enter') ? 'started' : 'stopped'; - this._target.on(actionState, handler); - return this; - } - - /** - * Gets whether the behavior is being targeted by the given user, or at all if no user is given. - * @param user The user to check whether they are targeting this behavior. - * @return True if the user is targeting this behavior, false if not. In the case where no user id is given, this - * returns true if any user is targeting this behavior, false if none are. - */ - public isTargeted(user?: User): boolean { - return this._target.isActive(user); - } -} +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { + ActionHandler, + ActionState, + Behavior, + BehaviorType, + DiscreteAction, + User +} from '../..'; + +/** + * Target behavior class containing the target behavior actions. + */ +export class TargetBehavior extends Behavior { + private _target: DiscreteAction = new DiscreteAction(); + + /** @inheritdoc */ + public get behaviorType(): BehaviorType { return 'target'; } + + public get target() { return this._target; } + + /** + * Add a target handler to be called when the given target state is triggered. + * @param targetState The target state to fire the handler on. + * @param handler The handler to call when the target state is triggered. + * @return This target behavior. + */ + public onTarget(targetState: 'enter' | 'exit', handler: ActionHandler): this { + const actionState: ActionState = (targetState === 'enter') ? 'started' : 'stopped'; + this._target.on(actionState, handler); + return this; + } + + /** + * Gets whether the behavior is being targeted by the given user, or at all if no user is given. + * @param user The user to check whether they are targeting this behavior. + * @return True if the user is targeting this behavior, false if not. In the case where no user id is given, this + * returns true if any user is targeting this behavior, false if none are. + */ + public isTargeted(user?: User): boolean { + return this._target.isActive(user); + } +} diff --git a/packages/sdk/src/types/runtime/index.ts b/packages/sdk/src/actor/index.ts similarity index 63% rename from packages/sdk/src/types/runtime/index.ts rename to packages/sdk/src/actor/index.ts index 1cbc43a42..11739f16e 100644 --- a/packages/sdk/src/types/runtime/index.ts +++ b/packages/sdk/src/actor/index.ts @@ -1,22 +1,17 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -export * from './actor'; -export * from './actorTransform'; -export * from './behaviors'; -export * from './context'; -export * from './transform'; -export * from './user'; -export * from './attachment'; -export * from './light'; -export * from './rigidbody'; -export * from './mediaInstance'; -export * from './text'; -export * from './collider'; -export * from './assets'; -export * from './physics'; -export * from './lookAt'; -export * from './appearance'; -export * from './groupMask'; +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +export * from './behaviors'; +export * from './media'; +export * from './physics'; + +export * from './actor'; +export * from './actorTransform'; +export * from './appearance'; +export * from './attachment'; +export * from './light'; +export * from './lookAt'; +export * from './text'; +export * from './transform'; diff --git a/packages/sdk/src/types/runtime/light.ts b/packages/sdk/src/actor/light.ts similarity index 93% rename from packages/sdk/src/types/runtime/light.ts rename to packages/sdk/src/actor/light.ts index 622cb3aec..e06eef54d 100644 --- a/packages/sdk/src/types/runtime/light.ts +++ b/packages/sdk/src/actor/light.ts @@ -1,62 +1,62 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -import { Color3, Color3Like } from '../..'; - -export type LightType = 'spot' | 'point'; - -export interface LightLike { - enabled: boolean; - type: LightType; - color: Partial; - intensity: number; - range: number; - spotAngle: number; -} - -export class Light implements LightLike { - public enabled = true; - public type: LightType = 'point'; - public intensity = 1; - // spot- and point-only: - public range = 1; - // spot-only: - public spotAngle = Math.PI / 4; - - private _color: Color3; - - public get color() { return this._color; } - public set color(value: Partial) { this._color.copy(value); } - - /** - * PUBLIC METHODS - */ - - constructor() { - this._color = Color3.White(); - } - - public copy(from: Partial): this { - if (!from) { return this; } - if (from.enabled !== undefined) { this.enabled = from.enabled; } - if (from.type !== undefined) { this.type = from.type; } - if (from.color !== undefined) { this.color.copy(from.color); } - if (from.range !== undefined) { this.range = from.range; } - if (from.intensity !== undefined) { this.intensity = from.intensity; } - if (from.spotAngle !== undefined) { this.spotAngle = from.spotAngle; } - return this; - } - - public toJSON() { - return { - enabled: this.enabled, - type: this.type, - color: this.color.toJSON(), - range: this.range, - intensity: this.intensity, - spotAngle: this.spotAngle, - } as LightLike; - } -} +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { Color3, Color3Like } from '..'; + +export type LightType = 'spot' | 'point'; + +export interface LightLike { + enabled: boolean; + type: LightType; + color: Partial; + intensity: number; + range: number; + spotAngle: number; +} + +export class Light implements LightLike { + public enabled = true; + public type: LightType = 'point'; + public intensity = 1; + // spot- and point-only: + public range = 1; + // spot-only: + public spotAngle = Math.PI / 4; + + private _color: Color3; + + public get color() { return this._color; } + public set color(value: Partial) { this._color.copy(value); } + + /** + * PUBLIC METHODS + */ + + constructor() { + this._color = Color3.White(); + } + + public copy(from: Partial): this { + if (!from) { return this; } + if (from.enabled !== undefined) { this.enabled = from.enabled; } + if (from.type !== undefined) { this.type = from.type; } + if (from.color !== undefined) { this.color.copy(from.color); } + if (from.range !== undefined) { this.range = from.range; } + if (from.intensity !== undefined) { this.intensity = from.intensity; } + if (from.spotAngle !== undefined) { this.spotAngle = from.spotAngle; } + return this; + } + + public toJSON() { + return { + enabled: this.enabled, + type: this.type, + color: this.color.toJSON(), + range: this.range, + intensity: this.intensity, + spotAngle: this.spotAngle, + } as LightLike; + } +} diff --git a/packages/sdk/src/types/runtime/lookAt.ts b/packages/sdk/src/actor/lookAt.ts similarity index 69% rename from packages/sdk/src/types/runtime/lookAt.ts rename to packages/sdk/src/actor/lookAt.ts index 0c710328c..2926f8e4c 100644 --- a/packages/sdk/src/types/runtime/lookAt.ts +++ b/packages/sdk/src/actor/lookAt.ts @@ -3,8 +3,28 @@ * Licensed under the MIT License. */ -import { LookAtMode } from "../.."; -import { Guid, ZeroGuid } from "../.."; +import { Guid, ZeroGuid } from ".."; + +/** + * Describes the ways in which an actor can face (point its local +Z axis toward) and track another object in the scene + */ +export enum LookAtMode { + + /** + * Actor is world-locked and does not rotate + */ + None = 'None', + + /** + * Actor rotates around its Y axis to face the target, offset by its rotation + */ + TargetY = 'TargetY', + + /** + * Actor rotates around its X and Y axes to face the target, offset by its rotation + */ + TargetXY = 'TargetXY' +} export interface LookAtLike { actorId: Guid; diff --git a/packages/sdk/src/media/index.ts b/packages/sdk/src/actor/media/index.ts similarity index 84% rename from packages/sdk/src/media/index.ts rename to packages/sdk/src/actor/media/index.ts index be5c8ccb0..db57faac9 100644 --- a/packages/sdk/src/media/index.ts +++ b/packages/sdk/src/actor/media/index.ts @@ -4,4 +4,5 @@ */ export * from './mediaCommand'; +export * from './mediaInstance'; export * from './setMediaStateOptions'; diff --git a/packages/sdk/src/media/mediaCommand.ts b/packages/sdk/src/actor/media/mediaCommand.ts similarity index 100% rename from packages/sdk/src/media/mediaCommand.ts rename to packages/sdk/src/actor/media/mediaCommand.ts diff --git a/packages/sdk/src/types/runtime/mediaInstance.ts b/packages/sdk/src/actor/media/mediaInstance.ts similarity index 98% rename from packages/sdk/src/types/runtime/mediaInstance.ts rename to packages/sdk/src/actor/media/mediaInstance.ts index 403a8196a..b60fc452d 100644 --- a/packages/sdk/src/types/runtime/mediaInstance.ts +++ b/packages/sdk/src/actor/media/mediaInstance.ts @@ -6,11 +6,11 @@ import { Actor, Guid, + log, MediaCommand, newGuid, SetMediaStateOptions } from '../..'; -import { log } from '../../log'; /** * A MediaInstance represents an instance managing the playback of a sound or video stream, diff --git a/packages/sdk/src/media/setMediaStateOptions.ts b/packages/sdk/src/actor/media/setMediaStateOptions.ts similarity index 100% rename from packages/sdk/src/media/setMediaStateOptions.ts rename to packages/sdk/src/actor/media/setMediaStateOptions.ts diff --git a/packages/sdk/src/types/runtime/collider.ts b/packages/sdk/src/actor/physics/collider.ts similarity index 89% rename from packages/sdk/src/types/runtime/collider.ts rename to packages/sdk/src/actor/physics/collider.ts index b70f418a9..3bda801e9 100644 --- a/packages/sdk/src/types/runtime/collider.ts +++ b/packages/sdk/src/actor/physics/collider.ts @@ -1,142 +1,148 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -import { Actor, ColliderGeometry } from '.'; -import { InternalCollider } from '../internal/collider'; -import { CollisionHandler, TriggerHandler } from './physics'; -import { ColliderEventType, CollisionEventType, TriggerEventType } from './physics/collisionEventType'; - -/** - * Controls what the assigned actors will collide with. - */ -export enum CollisionLayer { - /** - * Good for most actors. These will collide with all "physical" things: other default actors, - * navigation actors, and the non-MRE environment. It also blocks the UI cursor and receives press/grab events. - */ - Default = 'default', - /** - * For actors considered part of the environment. Can move/teleport onto these colliders, - * but cannot click or grab them. For example, the floor, an invisible wall, or an elevator platform. - */ - Navigation = 'navigation', - /** - * For "non-physical" actors. Only interact with the cursor (with press/grab events) and other holograms. - * For example, if you wanted a group of actors to behave as a separate physics simulation - * from the main scene. - */ - Hologram = 'hologram', - /** - * Actors in this layer do not collide with anything but the UI cursor. - */ - UI = 'ui' -} - -/** - * Describes the properties of a collider. - */ -export interface ColliderLike { - enabled: boolean; - isTrigger: boolean; - layer: CollisionLayer; - geometry: ColliderGeometry; - eventSubscriptions: ColliderEventType[]; -} - -/** - * A collider represents the abstraction of a physics collider object on the host. - */ -export class Collider implements ColliderLike { - public $DoNotObserve = ['_internal']; - - private _internal: InternalCollider; - - public enabled = true; - public isTrigger = false; - public layer = CollisionLayer.Default; - public geometry: Readonly; - - /** @hidden */ - public get internal() { return this._internal; } - - /** - * The current event subscriptions that are active on this collider. - */ - public get eventSubscriptions(): ColliderEventType[] { - return this.internal.eventSubscriptions; - } - - /** - * @hidden - * Creates a new Collider instance. - * @param $owner The owning actor instance. Field name is prefixed with a dollar sign so that it is ignored by - * the actor patch detection system. - * @param initFrom The collider like to use to init from. - */ - constructor(private $owner: Actor, from: Partial) { - if (from) { - if (!from.geometry || !from.geometry.shape) { - throw new Error("Must provide valid collider params containing a valid shape"); - } - - this._internal = new InternalCollider(this, $owner); - if (from.geometry !== undefined) { this.geometry = from.geometry; } - if (from.enabled !== undefined) { this.enabled = from.enabled; } - if (from.isTrigger !== undefined) { this.isTrigger = from.isTrigger; } - if (from.layer !== undefined) { this.layer = from.layer; } - } else { - throw new Error("Must provide a valid collider-like to initialize from."); - } - } - - /** - * Add a collision event handler for the given collision event state. - * @param eventType The type of the collision event. - * @param handler The handler to call when a collision event with the matching - * collision event state is received. - */ - public onCollision(eventType: CollisionEventType, handler: CollisionHandler) { - this.internal.on(eventType, handler); - } - - /** - * Remove the collision handler for the given collision event state. - * @param eventType The type of the collision event. - * @param handler The handler to remove. - */ - public offCollision(eventType: CollisionEventType, handler: CollisionHandler) { - this.internal.off(eventType, handler); - } - - /** - * Add a trigger event handler for the given collision event state. - * @param eventType The type of the trigger event. - * @param handler The handler to call when a trigger event with the matching - * collision event state is received. - */ - public onTrigger(eventType: TriggerEventType, handler: TriggerHandler) { - this.internal.on(eventType, handler); - } - - /** - * Remove the trigger handler for the given collision event state. - * @param eventType The type of the trigger event. - * @param handler The handler to remove. - */ - public offTrigger(eventType: TriggerEventType, handler: TriggerHandler) { - this.internal.off(eventType, handler); - } - - /** @hidden */ - public toJSON() { - return { - enabled: this.enabled, - isTrigger: this.isTrigger, - layer: this.layer, - geometry: this.geometry, - eventSubscriptions: this.eventSubscriptions - } as ColliderLike; - } -} +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { + Actor, + ColliderEventType, + ColliderGeometry, + CollisionEventType, + CollisionHandler, + TriggerEventType, + TriggerHandler +} from '../..'; +import { ColliderInternal } from './colliderInternal'; + +/** + * Controls what the assigned actors will collide with. + */ +export enum CollisionLayer { + /** + * Good for most actors. These will collide with all "physical" things: other default actors, + * navigation actors, and the non-MRE environment. It also blocks the UI cursor and receives press/grab events. + */ + Default = 'default', + /** + * For actors considered part of the environment. Can move/teleport onto these colliders, + * but cannot click or grab them. For example, the floor, an invisible wall, or an elevator platform. + */ + Navigation = 'navigation', + /** + * For "non-physical" actors. Only interact with the cursor (with press/grab events) and other holograms. + * For example, if you wanted a group of actors to behave as a separate physics simulation + * from the main scene. + */ + Hologram = 'hologram', + /** + * Actors in this layer do not collide with anything but the UI cursor. + */ + UI = 'ui' +} + +/** + * Describes the properties of a collider. + */ +export interface ColliderLike { + enabled: boolean; + isTrigger: boolean; + layer: CollisionLayer; + geometry: ColliderGeometry; + eventSubscriptions: ColliderEventType[]; +} + +/** + * A collider represents the abstraction of a physics collider object on the host. + */ +export class Collider implements ColliderLike { + public $DoNotObserve = ['_internal']; + + private _internal: ColliderInternal; + + public enabled = true; + public isTrigger = false; + public layer = CollisionLayer.Default; + public geometry: Readonly; + + /** @hidden */ + public get internal() { return this._internal; } + + /** + * The current event subscriptions that are active on this collider. + */ + public get eventSubscriptions(): ColliderEventType[] { + return this.internal.eventSubscriptions; + } + + /** + * @hidden + * Creates a new Collider instance. + * @param $owner The owning actor instance. Field name is prefixed with a dollar sign so that it is ignored by + * the actor patch detection system. + * @param initFrom The collider like to use to init from. + */ + constructor(private $owner: Actor, from: Partial) { + if (from) { + if (!from.geometry || !from.geometry.shape) { + throw new Error("Must provide valid collider params containing a valid shape"); + } + + this._internal = new ColliderInternal(this, $owner); + if (from.geometry !== undefined) { this.geometry = from.geometry; } + if (from.enabled !== undefined) { this.enabled = from.enabled; } + if (from.isTrigger !== undefined) { this.isTrigger = from.isTrigger; } + if (from.layer !== undefined) { this.layer = from.layer; } + } else { + throw new Error("Must provide a valid collider-like to initialize from."); + } + } + + /** + * Add a collision event handler for the given collision event state. + * @param eventType The type of the collision event. + * @param handler The handler to call when a collision event with the matching + * collision event state is received. + */ + public onCollision(eventType: CollisionEventType, handler: CollisionHandler) { + this.internal.on(eventType, handler); + } + + /** + * Remove the collision handler for the given collision event state. + * @param eventType The type of the collision event. + * @param handler The handler to remove. + */ + public offCollision(eventType: CollisionEventType, handler: CollisionHandler) { + this.internal.off(eventType, handler); + } + + /** + * Add a trigger event handler for the given collision event state. + * @param eventType The type of the trigger event. + * @param handler The handler to call when a trigger event with the matching + * collision event state is received. + */ + public onTrigger(eventType: TriggerEventType, handler: TriggerHandler) { + this.internal.on(eventType, handler); + } + + /** + * Remove the trigger handler for the given collision event state. + * @param eventType The type of the trigger event. + * @param handler The handler to remove. + */ + public offTrigger(eventType: TriggerEventType, handler: TriggerHandler) { + this.internal.off(eventType, handler); + } + + /** @hidden */ + public toJSON() { + return { + enabled: this.enabled, + isTrigger: this.isTrigger, + layer: this.layer, + geometry: this.geometry, + eventSubscriptions: this.eventSubscriptions + } as ColliderLike; + } +} diff --git a/packages/sdk/src/types/runtime/physics/colliderGeometry.ts b/packages/sdk/src/actor/physics/colliderGeometry.ts similarity index 92% rename from packages/sdk/src/types/runtime/physics/colliderGeometry.ts rename to packages/sdk/src/actor/physics/colliderGeometry.ts index 7e26198f1..5de49fc45 100644 --- a/packages/sdk/src/types/runtime/physics/colliderGeometry.ts +++ b/packages/sdk/src/actor/physics/colliderGeometry.ts @@ -3,8 +3,7 @@ * Licensed under the MIT License. */ -import { ColliderType } from '.'; -import { Vector3Like } from "../../.."; +import { ColliderType, Vector3Like } from "../.."; /** * Collider parameters specific to a sphere collider. diff --git a/packages/sdk/src/types/internal/collider.ts b/packages/sdk/src/actor/physics/colliderInternal.ts similarity index 93% rename from packages/sdk/src/types/internal/collider.ts rename to packages/sdk/src/actor/physics/colliderInternal.ts index 7f225a03d..890ec2f4e 100644 --- a/packages/sdk/src/types/internal/collider.ts +++ b/packages/sdk/src/actor/physics/colliderInternal.ts @@ -11,10 +11,10 @@ import { CollisionData, CollisionHandler, TriggerHandler -} from "../runtime"; +} from "../.."; /** @hidden */ -export class InternalCollider { +export class ColliderInternal { private _eventHandlers = new EventEmitter(); private _eventSubCount = 0; @@ -45,7 +45,7 @@ export class InternalCollider { } /** @hidden */ - public copyHandlers(other: InternalCollider): void { + public copyHandlers(other: ColliderInternal): void { for (const event of other._eventHandlers.eventNames()) { for (const handler of other._eventHandlers.listeners(event)) { this._eventHandlers.on(event, handler as (...args: any[]) => void); diff --git a/packages/sdk/src/types/runtime/physics/colliderType.ts b/packages/sdk/src/actor/physics/colliderType.ts similarity index 94% rename from packages/sdk/src/types/runtime/physics/colliderType.ts rename to packages/sdk/src/actor/physics/colliderType.ts index ebe19f631..93dafb2ef 100644 --- a/packages/sdk/src/types/runtime/physics/colliderType.ts +++ b/packages/sdk/src/actor/physics/colliderType.ts @@ -1,14 +1,14 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -/** - * The type of the collider. - */ -export enum ColliderType { - Auto = 'auto', - Box = 'box', - Sphere = 'sphere', - Capsule = 'capsule' -} +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +/** + * The type of the collider. + */ +export enum ColliderType { + Auto = 'auto', + Box = 'box', + Sphere = 'sphere', + Capsule = 'capsule' +} diff --git a/packages/sdk/src/types/runtime/physics/collision.ts b/packages/sdk/src/actor/physics/collision.ts similarity index 92% rename from packages/sdk/src/types/runtime/physics/collision.ts rename to packages/sdk/src/actor/physics/collision.ts index 6d446be48..41c5bc823 100644 --- a/packages/sdk/src/types/runtime/physics/collision.ts +++ b/packages/sdk/src/actor/physics/collision.ts @@ -1,53 +1,53 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -import { Actor, Guid, Vector3 } from "../../.."; - -/** - * The collision handler to be called when a collision event occurs. - * @param data The collision data associated with the collision. - */ -export type CollisionHandler = (data: CollisionData) => void; - -/** - * The trigger handler to be called whan an actor has entered or exited - * a trigger volume. - * @param otherActor The other actor that has entered the trigger volume. - */ -export type TriggerHandler = (otherActor: Actor) => void; - -/** - * The collision state for the collsion event. - */ -export type CollisionEventState = 'enter' | 'exit'; - -/** - * The point of contact for a collision. - */ -export interface ContactPoint { - normal: Vector3; - point: Vector3; - separation: number; -} - -/** - * The collision data collected when a collision occurs. - */ -export interface CollisionData { - otherActorId: Guid; - otherActor?: Actor; - contacts: ContactPoint[]; - impulse: Vector3; - relativeVelocity: Vector3; -} - -/** - * The layer that the collider should be assigned to. - */ -// export enum CollisionLayer { -// Object = 'object', -// Environment = 'environment', -// Hologram = 'hologram' -// } +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { Actor, Guid, Vector3 } from "../.."; + +/** + * The collision handler to be called when a collision event occurs. + * @param data The collision data associated with the collision. + */ +export type CollisionHandler = (data: CollisionData) => void; + +/** + * The trigger handler to be called whan an actor has entered or exited + * a trigger volume. + * @param otherActor The other actor that has entered the trigger volume. + */ +export type TriggerHandler = (otherActor: Actor) => void; + +/** + * The collision state for the collsion event. + */ +export type CollisionEventState = 'enter' | 'exit'; + +/** + * The point of contact for a collision. + */ +export interface ContactPoint { + normal: Vector3; + point: Vector3; + separation: number; +} + +/** + * The collision data collected when a collision occurs. + */ +export interface CollisionData { + otherActorId: Guid; + otherActor?: Actor; + contacts: ContactPoint[]; + impulse: Vector3; + relativeVelocity: Vector3; +} + +/** + * The layer that the collider should be assigned to. + */ +// export enum CollisionLayer { +// Object = 'object', +// Environment = 'environment', +// Hologram = 'hologram' +// } diff --git a/packages/sdk/src/types/runtime/physics/collisionDetectionMode.ts b/packages/sdk/src/actor/physics/collisionDetectionMode.ts similarity index 95% rename from packages/sdk/src/types/runtime/physics/collisionDetectionMode.ts rename to packages/sdk/src/actor/physics/collisionDetectionMode.ts index 3fde70238..b14c917fa 100644 --- a/packages/sdk/src/types/runtime/physics/collisionDetectionMode.ts +++ b/packages/sdk/src/actor/physics/collisionDetectionMode.ts @@ -1,13 +1,13 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -/** - * The collision detection mode of a rigid body. - */ -export enum CollisionDetectionMode { - Discrete = 'Discrete', - Continuous = 'Continuous', - ContinuousDynamic = 'ContinuousDynamic' -} +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +/** + * The collision detection mode of a rigid body. + */ +export enum CollisionDetectionMode { + Discrete = 'Discrete', + Continuous = 'Continuous', + ContinuousDynamic = 'ContinuousDynamic' +} diff --git a/packages/sdk/src/types/runtime/physics/collisionEvent.ts b/packages/sdk/src/actor/physics/collisionEvent.ts similarity index 74% rename from packages/sdk/src/types/runtime/physics/collisionEvent.ts rename to packages/sdk/src/actor/physics/collisionEvent.ts index 17e07a72d..2751c9571 100644 --- a/packages/sdk/src/types/runtime/physics/collisionEvent.ts +++ b/packages/sdk/src/actor/physics/collisionEvent.ts @@ -1,16 +1,15 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -import { CollisionData, CollisionEventType } from "."; -import { Guid } from '../../..'; - -/** - * A collision event that has occured between physics objects. - */ -export interface CollisionEvent { - colliderOwnerId: Guid; - eventType: CollisionEventType; - collisionData: CollisionData; -} +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { CollisionData, CollisionEventType, Guid } from '../..'; + +/** + * A collision event that has occured between physics objects. + */ +export interface CollisionEvent { + colliderOwnerId: Guid; + eventType: CollisionEventType; + collisionData: CollisionData; +} diff --git a/packages/sdk/src/types/runtime/physics/collisionEventType.ts b/packages/sdk/src/actor/physics/collisionEventType.ts similarity index 100% rename from packages/sdk/src/types/runtime/physics/collisionEventType.ts rename to packages/sdk/src/actor/physics/collisionEventType.ts diff --git a/packages/sdk/src/types/runtime/physics/index.ts b/packages/sdk/src/actor/physics/index.ts similarity index 78% rename from packages/sdk/src/types/runtime/physics/index.ts rename to packages/sdk/src/actor/physics/index.ts index c0b8ec2b1..43e5d835e 100644 --- a/packages/sdk/src/types/runtime/physics/index.ts +++ b/packages/sdk/src/actor/physics/index.ts @@ -1,12 +1,15 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -export * from './collision'; -export * from './collisionDetectionMode'; -export * from './collisionEvent'; -export * from './collisionEventType'; -export * from './colliderGeometry'; -export * from './colliderType'; -export * from './triggerEvent'; +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +export * from './collider'; +export * from './colliderGeometry'; +export * from './colliderType'; +export * from './collision'; +export * from './collisionDetectionMode'; +export * from './collisionEvent'; +export * from './collisionEventType'; +export * from './rigidbody'; +export * from './rigidBodyConstraints'; +export * from './triggerEvent'; diff --git a/packages/sdk/src/types/rigidBodyConstraints.ts b/packages/sdk/src/actor/physics/rigidBodyConstraints.ts similarity index 96% rename from packages/sdk/src/types/rigidBodyConstraints.ts rename to packages/sdk/src/actor/physics/rigidBodyConstraints.ts index 5511771ea..5056a9c4e 100644 --- a/packages/sdk/src/types/rigidBodyConstraints.ts +++ b/packages/sdk/src/actor/physics/rigidBodyConstraints.ts @@ -1,20 +1,20 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -/** - * Flags to constrain rigid body motion. - */ -export enum RigidBodyConstraints { - None = 'none', - FreezePositionX = 'freeze-position-x', - FreezePositionY = 'freeze-position-y', - FreezePositionZ = 'freeze-position-z', - FreezePosition = 'freeze-position', - FreezeRotationX = 'freeze-rotation-x', - FreezeRotationY = 'freeze-rotation-y', - FreezeRotationZ = 'freeze-rotation-z', - FreezeRotation = 'freeze-rotation', - FreezeAll = 'freeze-all', -} +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +/** + * Flags to constrain rigid body motion. + */ +export enum RigidBodyConstraints { + None = 'none', + FreezePositionX = 'freeze-position-x', + FreezePositionY = 'freeze-position-y', + FreezePositionZ = 'freeze-position-z', + FreezePosition = 'freeze-position', + FreezeRotationX = 'freeze-rotation-x', + FreezeRotationY = 'freeze-rotation-y', + FreezeRotationZ = 'freeze-rotation-z', + FreezeRotation = 'freeze-rotation', + FreezeAll = 'freeze-all', +} diff --git a/packages/sdk/src/types/runtime/rigidbody.ts b/packages/sdk/src/actor/physics/rigidbody.ts similarity index 88% rename from packages/sdk/src/types/runtime/rigidbody.ts rename to packages/sdk/src/actor/physics/rigidbody.ts index f2b4e2d34..5b2f28ce9 100644 --- a/packages/sdk/src/types/runtime/rigidbody.ts +++ b/packages/sdk/src/actor/physics/rigidbody.ts @@ -1,217 +1,215 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -import { Actor, CollisionDetectionMode } from '.'; -import { QuaternionLike, Vector3, Vector3Like } from '../..'; -import { - RigidBodyAddForce, - RigidBodyAddForceAtPosition, - RigidBodyAddRelativeTorque, - RigidBodyAddTorque, - RigidBodyMovePosition, - RigidBodyMoveRotation, -} from '../network/payloads'; -import { RigidBodyConstraints } from '../rigidBodyConstraints'; - -/** - * Describes the properties of a Rigid Body - */ -export interface RigidBodyLike { - /** Whether the rigid body is enabled or not. */ - enabled: boolean; - /** The velocity of the rigid body. */ - velocity: Partial; - /** The angular velocity of the rigid body. */ - angularVelocity: Partial; - /** The mass of the rigid body. */ - mass: number; - /** Whether to detect collisions with this rigid body. */ - detectCollisions: boolean; - /** The collision detection mode to use with this rigid body. @see CollisionDetectionMode for options. */ - collisionDetectionMode: CollisionDetectionMode; - /** Whether the rigid body is affected by gravity. */ - useGravity: boolean; - /** - * Whether the rigid body is kinematic. Note kinematic rigid bodies participate in collisions, - * but are not simulated by the rigid body. This is useful for objects that should collide with - * other objects, but you want to control the position/rotation manually or animate them. - */ - isKinematic: boolean; - /** The constraints that the rigid body is bound by. @see RigidBodyConstraints for options. */ - constraints: RigidBodyConstraints[]; -} - -/** - * Class that represents a rigid body found on an actor. - */ -export class RigidBody implements RigidBodyLike { - private _velocity: Vector3; - private _angularVelocity: Vector3; - private _constraints: RigidBodyConstraints[]; - - /** @inheritdoc */ - public enabled = true; - - /** @inheritdoc */ - public mass = 1.0; - - /** @inheritdoc */ - public detectCollisions = true; - - /** @inheritdoc */ - public collisionDetectionMode = CollisionDetectionMode.Discrete; - - /** @inheritdoc */ - public useGravity = true; - - /** @inheritdoc */ - public isKinematic = false; - - /** - * PUBLIC ACCESSORS - */ - - /** @inheritdoc */ - public get velocity() { return this._velocity; } - public set velocity(value: Partial) { this._velocity.copy(value); } - - /** @inheritdoc */ - public get angularVelocity() { return this._angularVelocity; } - public set angularVelocity(value: Partial) { this._angularVelocity.copy(value); } - - /** @inheritdoc */ - public get constraints() { return this._constraints; } - public set constraints(value: RigidBodyConstraints[]) { - this._constraints = [...value]; - // TODO: Figure out array patching - // this._changed("constraints"); - } - - /** - * PUBLIC METHODS - */ - - /** - * @hidden - * Creates a new RigidBody instance. - * @param $owner The owning actor instance. Field name is prefixed with a dollar sign so that it is ignored by - * the actor patch detection system. - */ - constructor(private $owner: Actor) { - this._velocity = Vector3.Zero(); - this._angularVelocity = Vector3.Zero(); - this._constraints = []; - } - - /** @hidden */ - public copy(from: Partial): this { - if (!from) { return this; } - if (from.enabled !== undefined) { this.enabled = from.enabled; } - if (from.velocity !== undefined) { this._velocity.copy(from.velocity); } - if (from.angularVelocity !== undefined) { this._angularVelocity.copy(from.angularVelocity); } - if (from.mass !== undefined) { this.mass = from.mass; } - if (from.detectCollisions !== undefined) { this.detectCollisions = from.detectCollisions; } - if (from.collisionDetectionMode !== undefined) { this.collisionDetectionMode = from.collisionDetectionMode; } - if (from.useGravity !== undefined) { this.useGravity = from.useGravity; } - if (from.isKinematic !== undefined) { this.isKinematic = from.isKinematic; } - if (from.constraints !== undefined) { this.constraints = from.constraints; } - return this; - } - - /** @hidden */ - public toJSON() { - return { - enabled: this.enabled, - velocity: this.velocity.toJSON(), - angularVelocity: this.angularVelocity.toJSON(), - mass: this.mass, - detectCollisions: this.detectCollisions, - collisionDetectionMode: this.collisionDetectionMode, - useGravity: this.useGravity, - isKinematic: this.isKinematic, - constraints: this.constraints, - } as RigidBodyLike; - } - - /** - * Move the rigid body to the new position with interpolation where supported. - * @param position The position to move to. - */ - public movePosition(position: Partial) { - this.$owner.context.internal.sendRigidBodyCommand( - this.$owner.id, - { - type: 'rigidbody-move-position', - position, - } as RigidBodyMovePosition); - } - - /** - * Rotate the rigid body to the new rotation with interpolation where supported. - * @param rotation The new rotation to rotate to. - */ - public moveRotation(rotation: QuaternionLike) { - this.$owner.context.internal.sendRigidBodyCommand( - this.$owner.id, - { - type: 'rigidbody-move-rotation', - rotation, - } as RigidBodyMoveRotation); - } - - /** - * Apply a force to the rigid body for physics to simulate. - * @param force The force to apply to the rigid body. - */ - public addForce(force: Partial) { - this.$owner.context.internal.sendRigidBodyCommand( - this.$owner.id, - { - type: 'rigidbody-add-force', - force, - } as RigidBodyAddForce); - } - - /** - * Apply a force to the rigid body at a specific position for physics to simulate. - * @param force The force to apply to the rigid body. - * @param position The position at which to apply the force. This should be in app coordinates. - */ - public addForceAtPosition(force: Partial, position: Partial) { - this.$owner.context.internal.sendRigidBodyCommand( - this.$owner.id, - { - type: 'rigidbody-add-force-at-position', - force, - position, - } as RigidBodyAddForceAtPosition); - } - - /** - * Add a torque to the rigid body for physics to simulate. - * @param torque The torque to apply to the rigid body. - */ - public addTorque(torque: Partial) { - this.$owner.context.internal.sendRigidBodyCommand( - this.$owner.id, - { - type: 'rigidbody-add-torque', - torque, - } as RigidBodyAddTorque); - } - - /** - * Add a relative torque to the rigid body for physics to simulate. - * @param relativeTorque The relative torque to apply to the rigid body. - */ - public addRelativeTorque(relativeTorque: Partial) { - this.$owner.context.internal.sendRigidBodyCommand( - this.$owner.id, - { - type: 'rigidbody-add-relative-torque', - relativeTorque, - } as RigidBodyAddRelativeTorque); - } -} +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { + Actor, + CollisionDetectionMode, + QuaternionLike, + RigidBodyConstraints, + Vector3, + Vector3Like +} from '../..'; +import { Payloads } from '../../internal'; + +/** + * Describes the properties of a Rigid Body + */ +export interface RigidBodyLike { + /** Whether the rigid body is enabled or not. */ + enabled: boolean; + /** The velocity of the rigid body. */ + velocity: Partial; + /** The angular velocity of the rigid body. */ + angularVelocity: Partial; + /** The mass of the rigid body. */ + mass: number; + /** Whether to detect collisions with this rigid body. */ + detectCollisions: boolean; + /** The collision detection mode to use with this rigid body. @see CollisionDetectionMode for options. */ + collisionDetectionMode: CollisionDetectionMode; + /** Whether the rigid body is affected by gravity. */ + useGravity: boolean; + /** + * Whether the rigid body is kinematic. Note kinematic rigid bodies participate in collisions, + * but are not simulated by the rigid body. This is useful for objects that should collide with + * other objects, but you want to control the position/rotation manually or animate them. + */ + isKinematic: boolean; + /** The constraints that the rigid body is bound by. @see RigidBodyConstraints for options. */ + constraints: RigidBodyConstraints[]; +} + +/** + * Class that represents a rigid body found on an actor. + */ +export class RigidBody implements RigidBodyLike { + private _velocity: Vector3; + private _angularVelocity: Vector3; + private _constraints: RigidBodyConstraints[]; + + /** @inheritdoc */ + public enabled = true; + + /** @inheritdoc */ + public mass = 1.0; + + /** @inheritdoc */ + public detectCollisions = true; + + /** @inheritdoc */ + public collisionDetectionMode = CollisionDetectionMode.Discrete; + + /** @inheritdoc */ + public useGravity = true; + + /** @inheritdoc */ + public isKinematic = false; + + /** + * PUBLIC ACCESSORS + */ + + /** @inheritdoc */ + public get velocity() { return this._velocity; } + public set velocity(value: Partial) { this._velocity.copy(value); } + + /** @inheritdoc */ + public get angularVelocity() { return this._angularVelocity; } + public set angularVelocity(value: Partial) { this._angularVelocity.copy(value); } + + /** @inheritdoc */ + public get constraints() { return this._constraints; } + public set constraints(value: RigidBodyConstraints[]) { + this._constraints = [...value]; + // TODO: Figure out array patching + // this._changed("constraints"); + } + + /** + * PUBLIC METHODS + */ + + /** + * @hidden + * Creates a new RigidBody instance. + * @param $owner The owning actor instance. Field name is prefixed with a dollar sign so that it is ignored by + * the actor patch detection system. + */ + constructor(private $owner: Actor) { + this._velocity = Vector3.Zero(); + this._angularVelocity = Vector3.Zero(); + this._constraints = []; + } + + /** @hidden */ + public copy(from: Partial): this { + if (!from) { return this; } + if (from.enabled !== undefined) { this.enabled = from.enabled; } + if (from.velocity !== undefined) { this._velocity.copy(from.velocity); } + if (from.angularVelocity !== undefined) { this._angularVelocity.copy(from.angularVelocity); } + if (from.mass !== undefined) { this.mass = from.mass; } + if (from.detectCollisions !== undefined) { this.detectCollisions = from.detectCollisions; } + if (from.collisionDetectionMode !== undefined) { this.collisionDetectionMode = from.collisionDetectionMode; } + if (from.useGravity !== undefined) { this.useGravity = from.useGravity; } + if (from.isKinematic !== undefined) { this.isKinematic = from.isKinematic; } + if (from.constraints !== undefined) { this.constraints = from.constraints; } + return this; + } + + /** @hidden */ + public toJSON() { + return { + enabled: this.enabled, + velocity: this.velocity.toJSON(), + angularVelocity: this.angularVelocity.toJSON(), + mass: this.mass, + detectCollisions: this.detectCollisions, + collisionDetectionMode: this.collisionDetectionMode, + useGravity: this.useGravity, + isKinematic: this.isKinematic, + constraints: this.constraints, + } as RigidBodyLike; + } + + /** + * Move the rigid body to the new position with interpolation where supported. + * @param position The position to move to. + */ + public movePosition(position: Partial) { + this.$owner.context.internal.sendRigidBodyCommand( + this.$owner.id, + { + type: 'rigidbody-move-position', + position, + } as Payloads.RigidBodyMovePosition); + } + + /** + * Rotate the rigid body to the new rotation with interpolation where supported. + * @param rotation The new rotation to rotate to. + */ + public moveRotation(rotation: QuaternionLike) { + this.$owner.context.internal.sendRigidBodyCommand( + this.$owner.id, + { + type: 'rigidbody-move-rotation', + rotation, + } as Payloads.RigidBodyMoveRotation); + } + + /** + * Apply a force to the rigid body for physics to simulate. + * @param force The force to apply to the rigid body. + */ + public addForce(force: Partial) { + this.$owner.context.internal.sendRigidBodyCommand( + this.$owner.id, + { + type: 'rigidbody-add-force', + force, + } as Payloads.RigidBodyAddForce); + } + + /** + * Apply a force to the rigid body at a specific position for physics to simulate. + * @param force The force to apply to the rigid body. + * @param position The position at which to apply the force. This should be in app coordinates. + */ + public addForceAtPosition(force: Partial, position: Partial) { + this.$owner.context.internal.sendRigidBodyCommand( + this.$owner.id, + { + type: 'rigidbody-add-force-at-position', + force, + position, + } as Payloads.RigidBodyAddForceAtPosition); + } + + /** + * Add a torque to the rigid body for physics to simulate. + * @param torque The torque to apply to the rigid body. + */ + public addTorque(torque: Partial) { + this.$owner.context.internal.sendRigidBodyCommand( + this.$owner.id, + { + type: 'rigidbody-add-torque', + torque, + } as Payloads.RigidBodyAddTorque); + } + + /** + * Add a relative torque to the rigid body for physics to simulate. + * @param relativeTorque The relative torque to apply to the rigid body. + */ + public addRelativeTorque(relativeTorque: Partial) { + this.$owner.context.internal.sendRigidBodyCommand( + this.$owner.id, + { + type: 'rigidbody-add-relative-torque', + relativeTorque, + } as Payloads.RigidBodyAddRelativeTorque); + } +} diff --git a/packages/sdk/src/types/runtime/physics/triggerEvent.ts b/packages/sdk/src/actor/physics/triggerEvent.ts similarity index 76% rename from packages/sdk/src/types/runtime/physics/triggerEvent.ts rename to packages/sdk/src/actor/physics/triggerEvent.ts index cfe5bf0d6..d965be778 100644 --- a/packages/sdk/src/types/runtime/physics/triggerEvent.ts +++ b/packages/sdk/src/actor/physics/triggerEvent.ts @@ -3,8 +3,7 @@ * Licensed under the MIT License. */ -import { Guid } from '../../..'; -import { TriggerEventType } from "./collisionEventType"; +import { Guid, TriggerEventType } from '../..'; /** * A trigger event that has occured between physics objects. diff --git a/packages/sdk/src/types/runtime/text.ts b/packages/sdk/src/actor/text.ts similarity index 94% rename from packages/sdk/src/types/runtime/text.ts rename to packages/sdk/src/actor/text.ts index 075471e03..deff0b862 100644 --- a/packages/sdk/src/types/runtime/text.ts +++ b/packages/sdk/src/actor/text.ts @@ -1,108 +1,108 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -import { Color3, Color3Like } from '../..'; - -export enum TextAnchorLocation { - TopLeft = 'top-left', - TopCenter = 'top-center', - TopRight = 'top-right', - MiddleLeft = 'middle-left', - MiddleCenter = 'middle-center', - MiddleRight = 'middle-right', - BottomLeft = 'bottom-left', - BottomCenter = 'bottom-center', - BottomRight = 'bottom-right', -} - -export enum TextJustify { - Left = 'left', - Center = 'center', - Right = 'right', -} - -export enum TextFontFamily { - Serif = 'serif', - SansSerif = 'sans-serif', -} - -export interface TextLike { - enabled: boolean; - contents: string; - height: number; - pixelsPerLine: number; - anchor: TextAnchorLocation; - justify: TextJustify; - font: TextFontFamily; - color: Partial; -} - -export class Text implements TextLike { - private _color: Color3; - - /** - * Whether or not to draw the text - */ - public enabled = true; - /** - * The text string to be drawn - */ - public contents = ''; - /** - * The height in meters of a line of text - */ - public height = 1; - /** - * The vertical resolution of a single line of text - */ - public pixelsPerLine = 50; - /** - * The position of the text anchor relative to the block of text - */ - public anchor: TextAnchorLocation = TextAnchorLocation.TopLeft; - /** - * The alignment of each text line relative to the others - */ - public justify: TextJustify = TextJustify.Left; - /** - * The font family to use to draw the text - */ - public font: TextFontFamily = TextFontFamily.SansSerif; - /** - * The text's color - */ - public get color() { return this._color; } - public set color(value: Partial) { this._color.copy(value); } - - constructor() { - this._color = Color3.White(); - } - - public copy(from: Partial): this { - if (!from) { return this; } - if (from.enabled !== undefined) { this.enabled = from.enabled; } - if (from.contents !== undefined) { this.contents = from.contents; } - if (from.height !== undefined) { this.height = from.height; } - if (from.pixelsPerLine !== undefined) { this.pixelsPerLine = from.pixelsPerLine; } - if (from.anchor !== undefined) { this.anchor = from.anchor; } - if (from.justify !== undefined) { this.justify = from.justify; } - if (from.font !== undefined) { this.font = from.font; } - if (from.color !== undefined) { this.color = from.color; } - return this; - } - - public toJSON() { - return { - enabled: this.enabled, - contents: this.contents, - height: this.height, - pixelsPerLine: this.pixelsPerLine, - anchor: this.anchor, - justify: this.justify, - font: this.font, - color: this.color.toJSON(), - } as TextLike; - } -} +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { Color3, Color3Like } from '..'; + +export enum TextAnchorLocation { + TopLeft = 'top-left', + TopCenter = 'top-center', + TopRight = 'top-right', + MiddleLeft = 'middle-left', + MiddleCenter = 'middle-center', + MiddleRight = 'middle-right', + BottomLeft = 'bottom-left', + BottomCenter = 'bottom-center', + BottomRight = 'bottom-right', +} + +export enum TextJustify { + Left = 'left', + Center = 'center', + Right = 'right', +} + +export enum TextFontFamily { + Serif = 'serif', + SansSerif = 'sans-serif', +} + +export interface TextLike { + enabled: boolean; + contents: string; + height: number; + pixelsPerLine: number; + anchor: TextAnchorLocation; + justify: TextJustify; + font: TextFontFamily; + color: Partial; +} + +export class Text implements TextLike { + private _color: Color3; + + /** + * Whether or not to draw the text + */ + public enabled = true; + /** + * The text string to be drawn + */ + public contents = ''; + /** + * The height in meters of a line of text + */ + public height = 1; + /** + * The vertical resolution of a single line of text + */ + public pixelsPerLine = 50; + /** + * The position of the text anchor relative to the block of text + */ + public anchor: TextAnchorLocation = TextAnchorLocation.TopLeft; + /** + * The alignment of each text line relative to the others + */ + public justify: TextJustify = TextJustify.Left; + /** + * The font family to use to draw the text + */ + public font: TextFontFamily = TextFontFamily.SansSerif; + /** + * The text's color + */ + public get color() { return this._color; } + public set color(value: Partial) { this._color.copy(value); } + + constructor() { + this._color = Color3.White(); + } + + public copy(from: Partial): this { + if (!from) { return this; } + if (from.enabled !== undefined) { this.enabled = from.enabled; } + if (from.contents !== undefined) { this.contents = from.contents; } + if (from.height !== undefined) { this.height = from.height; } + if (from.pixelsPerLine !== undefined) { this.pixelsPerLine = from.pixelsPerLine; } + if (from.anchor !== undefined) { this.anchor = from.anchor; } + if (from.justify !== undefined) { this.justify = from.justify; } + if (from.font !== undefined) { this.font = from.font; } + if (from.color !== undefined) { this.color = from.color; } + return this; + } + + public toJSON() { + return { + enabled: this.enabled, + contents: this.contents, + height: this.height, + pixelsPerLine: this.pixelsPerLine, + anchor: this.anchor, + justify: this.justify, + font: this.font, + color: this.color.toJSON(), + } as TextLike; + } +} diff --git a/packages/sdk/src/types/runtime/transform.ts b/packages/sdk/src/actor/transform.ts similarity index 95% rename from packages/sdk/src/types/runtime/transform.ts rename to packages/sdk/src/actor/transform.ts index 5152bb9ce..27310d8d4 100644 --- a/packages/sdk/src/types/runtime/transform.ts +++ b/packages/sdk/src/actor/transform.ts @@ -1,75 +1,75 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ -/* eslint-disable max-classes-per-file */ - -import { Quaternion, QuaternionLike, Vector3, Vector3Like } from '../..'; - -export interface TransformLike { - position: Partial; - rotation: Partial; -} - -export interface ScaledTransformLike extends TransformLike { - scale: Partial; -} - -export class Transform implements TransformLike { - private _position: Vector3; - private _rotation: Quaternion; - - public get position() { return this._position; } - public set position(value: Vector3) { this._position.copy(value); } - public get rotation() { return this._rotation; } - public set rotation(value: Quaternion) { this._rotation.copy(value); } - - /** - * PUBLIC METHODS - */ - - constructor() { - this._position = Vector3.Zero(); - this._rotation = Quaternion.Identity(); - } - - public copy(from: Partial): this { - if (!from) { return this; } - if (from.position !== undefined) { this.position.copy(from.position); } - if (from.rotation !== undefined) { this.rotation.copy(from.rotation); } - - return this; - } - - public toJSON() { - return { - position: this.position.toJSON(), - rotation: this.rotation.toJSON(), - } as TransformLike; - } -} - -export class ScaledTransform extends Transform implements ScaledTransformLike { - private _scale: Vector3; - - public get scale() { return this._scale; } - public set scale(value: Vector3) { this._scale.copy(value); } - - constructor() { - super(); - this._scale = Vector3.One(); - } - - public copy(from: Partial): this { - super.copy(from); - if (from.scale !== undefined) { this.scale.copy(from.scale); } - return this; - } - - public toJSON() { - return { - ...super.toJSON(), - scale: this.scale.toJSON(), - } as ScaledTransformLike; - } -} +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +/* eslint-disable max-classes-per-file */ + +import { Quaternion, QuaternionLike, Vector3, Vector3Like } from '..'; + +export interface TransformLike { + position: Partial; + rotation: Partial; +} + +export interface ScaledTransformLike extends TransformLike { + scale: Partial; +} + +export class Transform implements TransformLike { + private _position: Vector3; + private _rotation: Quaternion; + + public get position() { return this._position; } + public set position(value: Vector3) { this._position.copy(value); } + public get rotation() { return this._rotation; } + public set rotation(value: Quaternion) { this._rotation.copy(value); } + + /** + * PUBLIC METHODS + */ + + constructor() { + this._position = Vector3.Zero(); + this._rotation = Quaternion.Identity(); + } + + public copy(from: Partial): this { + if (!from) { return this; } + if (from.position !== undefined) { this.position.copy(from.position); } + if (from.rotation !== undefined) { this.rotation.copy(from.rotation); } + + return this; + } + + public toJSON() { + return { + position: this.position.toJSON(), + rotation: this.rotation.toJSON(), + } as TransformLike; + } +} + +export class ScaledTransform extends Transform implements ScaledTransformLike { + private _scale: Vector3; + + public get scale() { return this._scale; } + public set scale(value: Vector3) { this._scale.copy(value); } + + constructor() { + super(); + this._scale = Vector3.One(); + } + + public copy(from: Partial): this { + super.copy(from); + if (from.scale !== undefined) { this.scale.copy(from.scale); } + return this; + } + + public toJSON() { + return { + ...super.toJSON(), + scale: this.scale.toJSON(), + } as ScaledTransformLike; + } +} diff --git a/packages/sdk/src/animation/animation.ts b/packages/sdk/src/animation/animation.ts index c7f4cae11..ac3cace6e 100644 --- a/packages/sdk/src/animation/animation.ts +++ b/packages/sdk/src/animation/animation.ts @@ -2,12 +2,17 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ -import { Guid } from '../types/guid'; -import { AnimationWrapMode, InternalAnimation } from '.'; -import { Patchable } from '../types/patchable'; -import { Context } from '../types/runtime'; -import { ExportedPromise } from '../utils/exportedPromise'; -import readPath from '../utils/readPath'; +import { + AnimationWrapMode, + Context, + Guid, +} from '..'; +import { + ExportedPromise, + Patchable, + readPath +} from '../internal'; +import { AnimationInternal } from './animationInternal'; /** A serialized animation definition */ export interface AnimationLike { @@ -35,7 +40,7 @@ export interface AnimationLike { /** A runtime animation */ export class Animation implements AnimationLike, Patchable { /** @hidden */ - public internal = new InternalAnimation(this); + public internal = new AnimationInternal(this); private _id: Guid; /** @inheritdoc */ diff --git a/packages/sdk/src/animation/internalAnimation.ts b/packages/sdk/src/animation/animationInternal.ts similarity index 70% rename from packages/sdk/src/animation/internalAnimation.ts rename to packages/sdk/src/animation/animationInternal.ts index 9dd22811b..70b976479 100644 --- a/packages/sdk/src/animation/internalAnimation.ts +++ b/packages/sdk/src/animation/animationInternal.ts @@ -2,11 +2,11 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ -import { Animation, AnimationLike } from '.'; -import { InternalPatchable } from '../types/patchable'; +import { Animation, AnimationLike } from '..'; +import { InternalPatchable } from '../internal'; /** @hidden */ -export class InternalAnimation implements InternalPatchable { +export class AnimationInternal implements InternalPatchable { public observing = true; public patch: Partial; diff --git a/packages/sdk/src/animation/createAnimationOptions.ts b/packages/sdk/src/animation/createAnimationOptions.ts index 6a0f5b211..995e77a3f 100644 --- a/packages/sdk/src/animation/createAnimationOptions.ts +++ b/packages/sdk/src/animation/createAnimationOptions.ts @@ -8,7 +8,7 @@ import { AnimationKeyframe, AnimationWrapMode, SetAnimationStateOptions -} from '.'; +} from '..'; /** * Parameters to the `actor.createAnimation` call. diff --git a/packages/sdk/src/animation/index.ts b/packages/sdk/src/animation/index.ts index e4295a108..356438433 100644 --- a/packages/sdk/src/animation/index.ts +++ b/packages/sdk/src/animation/index.ts @@ -4,11 +4,10 @@ */ export * from './animation'; -export * from './animationWrapMode'; -export * from './animationKeyframe'; +export * from './animationEaseCurves'; export * from './animationEvent'; +export * from './animationKeyframe'; export * from './animationState'; +export * from './animationWrapMode'; export * from './createAnimationOptions'; export * from './setAnimationStateOptions'; -export * from './animationEaseCurves'; -export * from './internalAnimation'; diff --git a/packages/sdk/src/types/runtime/assets/asset.ts b/packages/sdk/src/asset/asset.ts similarity index 95% rename from packages/sdk/src/types/runtime/assets/asset.ts rename to packages/sdk/src/asset/asset.ts index 48be5d961..182fbde2c 100644 --- a/packages/sdk/src/types/runtime/assets/asset.ts +++ b/packages/sdk/src/asset/asset.ts @@ -1,174 +1,175 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -import { - AssetContainer, - Material, - MaterialLike, - Mesh, - MeshLike, - Prefab, - PrefabLike, - Sound, - SoundLike, - Texture, - TextureLike, - VideoStream, - VideoStreamLike -} from '.'; -import { Actor, Guid } from '../../..'; - -/** - * Instructions for how to load an asset. - */ -export interface AssetSource { - /** - * The format of the asset container. - */ - containerType: 'gltf' | 'library'; - - /** - * The URI at which the asset container can be found. - */ - uri?: string; - - /** - * A designator for which asset in the container this is. Format will be different for each container type. - * For example, a glTF's third material would have "materials/2" as its internalId. - */ - internalId?: string; -} - -export interface AssetLike { - /** - * The unique id of this asset. Use this to reference this asset in actors, etc. - */ - id: Guid; - /** - * A human-readable string identifying the asset. Not required to be unique, but - * can be referenced by name if it is. - */ - name?: string; - /** - * Where this asset came from. Used for loading on late-joining clients. - */ - source?: AssetSource; - - /** Only populated when this asset is a prefab. An asset will have only one of these types specified. */ - prefab?: Partial; - /** Only populated when this asset is a mesh. An asset will have only one of these types specified. */ - mesh?: Partial; - /** Only populated when this asset is a material. An asset will have only one of these types specified. */ - material?: Partial; - /** Only populated when this asset is a texture. An asset will have only one of these types specified. */ - texture?: Partial; - /** Only populated when this asset is a sound. An asset will have only one of these types specified. */ - sound?: Partial; - /** Only populated when this asset is a video stream. An asset will have only one of these types specified. */ - videoStream?: Partial; -} - -/** The base class for all asset types. */ -export abstract class Asset implements AssetLike { - private _id: Guid; - private _name: string; - private _source: AssetSource; - private _loadedPromise: Promise; - - /** @inheritdoc */ - public get id() { return this._id; } - - /** @inheritdoc */ - public get name() { return this._name; } - - /** @inheritdoc */ - public get source() { return this._source; } - - /** @inheritdoc */ - public get prefab(): Prefab { return null; } - /** @inheritdoc */ - public get mesh(): Mesh { return null; } - /** @inheritdoc */ - public get material(): Material { return null; } - /** @inheritdoc */ - public get texture(): Texture { return null; } - /** @inheritdoc */ - public get sound(): Sound { return null; } - - /** A promise that resolves when the asset is finished loading */ - public get created() { return this._loadedPromise; } - - /** Stores which actors/assets refer to this asset */ - protected references = new Set(); - - protected constructor(public container: AssetContainer, def: Partial) { - this._id = def.id; - this._name = def.name; - this._source = def.source; - } - - /** @hidden */ - public addReference(ref: Actor | Asset) { - this.references.add(ref); - } - - /** @hidden */ - public clearReference(ref: Actor | Asset) { - this.references.delete(ref); - } - - /** @hidden */ - public breakReference(ref: Actor | Asset): void { } - - /** @hidden */ - public breakAllReferences() { - for (const r of this.references) { - this.breakReference(r); - this.clearReference(r); - } - } - - /** @hidden */ - public setLoadedPromise(p: Promise) { - this._loadedPromise = p; - } - - /** @hidden */ - protected toJSON(): AssetLike { - return { - id: this._id, - name: this._name, - source: this._source - }; - } - - /** @hidden */ - public copy(from: Partial): this { - if (from.id) { this._id = from.id; } - if (from.name) { this._name = from.name; } - if (from.source) { this._source = from.source; } - - return this; - } - - /** @hidden */ - public static Parse(container: AssetContainer, def: AssetLike): Asset { - if (def.prefab) { - return new Prefab(container, def); - } else if (def.mesh) { - return new Mesh(container, def); - } else if (def.material) { - return new Material(container, def); - } else if (def.texture) { - return new Texture(container, def); - } else if (def.sound) { - return new Sound(container, def); - } else if (def.videoStream) { - return new VideoStream(container, def); - } else { - throw new Error(`Asset ${def.id} is not of a known type.`); - } - } -} +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { + Actor, + AssetContainer, + Guid, + Material, + MaterialLike, + Mesh, + MeshLike, + Prefab, + PrefabLike, + Sound, + SoundLike, + Texture, + TextureLike, + VideoStream, + VideoStreamLike +} from '..'; + +/** + * Instructions for how to load an asset. + */ +export interface AssetSource { + /** + * The format of the asset container. + */ + containerType: 'gltf' | 'library'; + + /** + * The URI at which the asset container can be found. + */ + uri?: string; + + /** + * A designator for which asset in the container this is. Format will be different for each container type. + * For example, a glTF's third material would have "materials/2" as its internalId. + */ + internalId?: string; +} + +export interface AssetLike { + /** + * The unique id of this asset. Use this to reference this asset in actors, etc. + */ + id: Guid; + /** + * A human-readable string identifying the asset. Not required to be unique, but + * can be referenced by name if it is. + */ + name?: string; + /** + * Where this asset came from. Used for loading on late-joining clients. + */ + source?: AssetSource; + + /** Only populated when this asset is a prefab. An asset will have only one of these types specified. */ + prefab?: Partial; + /** Only populated when this asset is a mesh. An asset will have only one of these types specified. */ + mesh?: Partial; + /** Only populated when this asset is a material. An asset will have only one of these types specified. */ + material?: Partial; + /** Only populated when this asset is a texture. An asset will have only one of these types specified. */ + texture?: Partial; + /** Only populated when this asset is a sound. An asset will have only one of these types specified. */ + sound?: Partial; + /** Only populated when this asset is a video stream. An asset will have only one of these types specified. */ + videoStream?: Partial; +} + +/** The base class for all asset types. */ +export abstract class Asset implements AssetLike { + private _id: Guid; + private _name: string; + private _source: AssetSource; + private _loadedPromise: Promise; + + /** @inheritdoc */ + public get id() { return this._id; } + + /** @inheritdoc */ + public get name() { return this._name; } + + /** @inheritdoc */ + public get source() { return this._source; } + + /** @inheritdoc */ + public get prefab(): Prefab { return null; } + /** @inheritdoc */ + public get mesh(): Mesh { return null; } + /** @inheritdoc */ + public get material(): Material { return null; } + /** @inheritdoc */ + public get texture(): Texture { return null; } + /** @inheritdoc */ + public get sound(): Sound { return null; } + + /** A promise that resolves when the asset is finished loading */ + public get created() { return this._loadedPromise; } + + /** Stores which actors/assets refer to this asset */ + protected references = new Set(); + + protected constructor(public container: AssetContainer, def: Partial) { + this._id = def.id; + this._name = def.name; + this._source = def.source; + } + + /** @hidden */ + public addReference(ref: Actor | Asset) { + this.references.add(ref); + } + + /** @hidden */ + public clearReference(ref: Actor | Asset) { + this.references.delete(ref); + } + + /** @hidden */ + public breakReference(ref: Actor | Asset): void { } + + /** @hidden */ + public breakAllReferences() { + for (const r of this.references) { + this.breakReference(r); + this.clearReference(r); + } + } + + /** @hidden */ + public setLoadedPromise(p: Promise) { + this._loadedPromise = p; + } + + /** @hidden */ + protected toJSON(): AssetLike { + return { + id: this._id, + name: this._name, + source: this._source + }; + } + + /** @hidden */ + public copy(from: Partial): this { + if (from.id) { this._id = from.id; } + if (from.name) { this._name = from.name; } + if (from.source) { this._source = from.source; } + + return this; + } + + /** @hidden */ + public static Parse(container: AssetContainer, def: AssetLike): Asset { + if (def.prefab) { + return new Prefab(container, def); + } else if (def.mesh) { + return new Mesh(container, def); + } else if (def.material) { + return new Material(container, def); + } else if (def.texture) { + return new Texture(container, def); + } else if (def.sound) { + return new Sound(container, def); + } else if (def.videoStream) { + return new VideoStream(container, def); + } else { + throw new Error(`Asset ${def.id} is not of a known type.`); + } + } +} diff --git a/packages/sdk/src/types/runtime/assets/assetContainer.ts b/packages/sdk/src/asset/assetContainer.ts similarity index 97% rename from packages/sdk/src/types/runtime/assets/assetContainer.ts rename to packages/sdk/src/asset/assetContainer.ts index 515fc92a9..1e4b4583d 100644 --- a/packages/sdk/src/types/runtime/assets/assetContainer.ts +++ b/packages/sdk/src/asset/assetContainer.ts @@ -5,25 +5,25 @@ import { Asset, AssetSource, + Context, + Guid, + log, Material, MaterialLike, Mesh, + newGuid, Prefab, + PrimitiveDefinition, + PrimitiveShape, + ReadonlyMap, Sound, SoundLike, Texture, TextureLike, + Vector3Like, VideoStream, VideoStreamLike -} from '.'; +} from '..'; import { - Context, - Guid, - newGuid, - ReadonlyMap, - PrimitiveDefinition, - PrimitiveShape, - Vector3Like -} from '../../..'; -import { log } from '../../../log'; -import resolveJsonValues from '../../../utils/resolveJsonValues'; -import * as Payloads from '../../network/payloads'; + Payloads, + resolveJsonValues +} from '../internal'; /** * The root object of the MRE SDK's asset system. Once you create an AssetContainer, diff --git a/packages/sdk/src/types/internal/asset.ts b/packages/sdk/src/asset/assetInternal.ts similarity index 71% rename from packages/sdk/src/types/internal/asset.ts rename to packages/sdk/src/asset/assetInternal.ts index 213e93401..c9057c815 100644 --- a/packages/sdk/src/types/internal/asset.ts +++ b/packages/sdk/src/asset/assetInternal.ts @@ -3,13 +3,13 @@ * Licensed under the MIT License. */ -import { Asset, AssetLike } from '../..'; -import { InternalPatchable } from '../patchable'; +import { Asset, AssetLike } from '..'; +import { InternalPatchable } from '../internal'; /** * @hidden */ -export class InternalAsset implements InternalPatchable { +export class AssetInternal implements InternalPatchable { public observing = true; public patch: AssetLike; diff --git a/packages/sdk/src/types/runtime/assets/assetIterator.ts b/packages/sdk/src/asset/assetIterator.ts similarity index 95% rename from packages/sdk/src/types/runtime/assets/assetIterator.ts rename to packages/sdk/src/asset/assetIterator.ts index 11372cda9..67baf6a56 100644 --- a/packages/sdk/src/types/runtime/assets/assetIterator.ts +++ b/packages/sdk/src/asset/assetIterator.ts @@ -5,7 +5,7 @@ /* eslint-disable max-classes-per-file */ -import { Asset, AssetContainer } from '.'; +import { Asset, AssetContainer } from '..'; /** @hidden */ export class AssetContainerIterator implements Iterator { diff --git a/packages/sdk/src/types/runtime/assets/index.ts b/packages/sdk/src/asset/index.ts similarity index 96% rename from packages/sdk/src/types/runtime/assets/index.ts rename to packages/sdk/src/asset/index.ts index 1ab4a723f..6f220bb6b 100644 --- a/packages/sdk/src/types/runtime/assets/index.ts +++ b/packages/sdk/src/asset/index.ts @@ -1,14 +1,14 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -export * from './asset'; -export * from './material'; -export * from './mesh'; -export * from './prefab'; -export * from './sound'; -export * from './texture'; -export * from './videoStream'; -export * from './assetIterator'; -export * from './assetContainer'; +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +export * from './asset'; +export * from './assetContainer'; +export * from './assetIterator'; +export * from './material'; +export * from './mesh'; +export * from './prefab'; +export * from './sound'; +export * from './texture'; +export * from './videoStream'; diff --git a/packages/sdk/src/types/runtime/assets/material.ts b/packages/sdk/src/asset/material.ts similarity index 90% rename from packages/sdk/src/types/runtime/assets/material.ts rename to packages/sdk/src/asset/material.ts index 80f2aa138..3b3788b89 100644 --- a/packages/sdk/src/types/runtime/assets/material.ts +++ b/packages/sdk/src/asset/material.ts @@ -1,211 +1,223 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -import { Asset, AssetContainer, AssetLike } from '.'; -import { Actor, Guid, ZeroGuid } from '../../..'; -import { Color3, Color4, Color4Like, Vector2, Vector2Like } from '../../../math'; -import { observe } from '../../../utils/observe'; -import readPath from '../../../utils/readPath'; -import { InternalAsset } from '../../internal/asset'; -import { Patchable } from '../../patchable'; - -/** - * Describes the properties of a Material. - */ -export interface MaterialLike { - /** The base color of this material. */ - color: Partial; - /** The main (albedo) texture asset ID */ - mainTextureId: Guid; - /** The main texture's offset from default */ - mainTextureOffset: Vector2Like; - /** The main texture's scale from default */ - mainTextureScale: Vector2Like; - /** How the color/texture's alpha channel should be handled */ - alphaMode: AlphaMode; - /** Visibility threshold in masked alpha mode */ - alphaCutoff: number; -} - -/** - * Controls how transparency is handled. - */ -export enum AlphaMode { - /** The object is rendered opaque, and transparency info is discarded. */ - Opaque = 'opaque', - /** - * Any parts with alpha above a certain cutoff ([[Material.alphaCutoff]]) - * will be rendered solid. Everything else is fully transparent. - */ - Mask = 'mask', - /** - * A pixel's transparency is directly proportional to its alpha value. - */ - Blend = 'blend' -} - -/** - * Represents a material on a mesh. - */ -export class Material extends Asset implements MaterialLike, Patchable { - private _color = Color4.FromColor3(Color3.White(), 1.0); - private _mainTextureId = ZeroGuid; - private _mainTextureOffset = Vector2.Zero(); - private _mainTextureScale = Vector2.One(); - private _alphaMode = AlphaMode.Opaque; - private _alphaCutoff = 0.5; - private _internal = new InternalAsset(this); - - /** @hidden */ - public get internal() { return this._internal; } - - /** @inheritdoc */ - public get color() { return this._color; } - public set color(value) { if (value) { this._color.copy(value); } } - - /** @returns A shared reference to this material's texture asset */ - public get mainTexture() { - return this.container.context.internal.lookupAsset(this._mainTextureId)?.texture; - } - public set mainTexture(value) { - this.mainTextureId = value?.id ?? ZeroGuid; - } - - /** @inheritdoc */ - public get mainTextureId() { return this._mainTextureId; } - public set mainTextureId(value) { - if (!value) { - value = ZeroGuid; - } - if (!this.container.context.internal.lookupAsset(value)) { - value = ZeroGuid; // throw? - } - - if (value === this._mainTextureId) { return; } - - if (this.mainTexture) { - this.mainTexture.clearReference(this); - } - this._mainTextureId = value; - if (this.mainTexture) { - this.mainTexture.addReference(this); - } - this.materialChanged('mainTextureId'); - } - - /** @inheritdoc */ - public get mainTextureOffset() { return this._mainTextureOffset; } - public set mainTextureOffset(value) { if (value) { this._mainTextureOffset.copy(value); } } - - /** @inheritdoc */ - public get mainTextureScale() { return this._mainTextureScale; } - public set mainTextureScale(value) { if (value) { this._mainTextureScale.copy(value); } } - - /** @inheritdoc */ - public get alphaMode() { return this._alphaMode; } - public set alphaMode(value) { this._alphaMode = value; this.materialChanged('alphaMode'); } - - /** @inheritdoc */ - public get alphaCutoff() { return this._alphaCutoff; } - public set alphaCutoff(value) { this._alphaCutoff = value; this.materialChanged('alphaCutoff'); } - - /** @inheritdoc */ - public get material(): Material { return this; } - - /** INTERNAL USE ONLY. To create a new material from scratch, use [[AssetManager.createMaterial]]. */ - public constructor(container: AssetContainer, def: AssetLike) { - super(container, def); - - if (!def.material) { - throw new Error("Cannot construct material from non-material definition"); - } - - this.copy(def); - - // material patching: observe the nested material properties - // for changed values, and write them to a patch - observe({ - target: this._color, - targetName: 'color', - notifyChanged: (...path: string[]) => this.materialChanged(...path) - }); - observe({ - target: this._mainTextureOffset, - targetName: 'mainTextureOffset', - notifyChanged: (...path: string[]) => this.materialChanged(...path) - }); - observe({ - target: this._mainTextureScale, - targetName: 'mainTextureScale', - notifyChanged: (...path: string[]) => this.materialChanged(...path) - }); - } - - public copy(from: Partial): this { - if (!from) { - return this; - } - - // Pause change detection while we copy the values into the actor. - const wasObserving = this.internal.observing; - this.internal.observing = false; - - super.copy(from); - if (from.material) { - if (from.material.color) { - this.color.copy(from.material.color); - } - if (from.material.mainTextureOffset) { - this.mainTextureOffset.copy(from.material.mainTextureOffset); - } - if (from.material.mainTextureScale) { - this.mainTextureScale.copy(from.material.mainTextureScale); - } - if (from.material.mainTextureId) { - this.mainTextureId = from.material.mainTextureId; - } - if (from.material.alphaMode) { - this.alphaMode = from.material.alphaMode; - } - if (from.material.alphaCutoff) { - this.alphaCutoff = from.material.alphaCutoff; - } - } - - this.internal.observing = wasObserving; - return this; - } - - /** @hidden */ - public toJSON(): AssetLike { - return { - ...super.toJSON(), - material: { - color: this.color.toJSON(), - mainTextureId: this.mainTextureId, - mainTextureOffset: this.mainTextureOffset.toJSON(), - mainTextureScale: this.mainTextureScale.toJSON(), - alphaMode: this.alphaMode, - alphaCutoff: this.alphaCutoff - } - }; - } - - private materialChanged(...path: string[]): void { - if (this.internal.observing) { - this.container.context.internal.incrementGeneration(); - this.internal.patch = this.internal.patch || { material: {} } as AssetLike; - readPath(this, this.internal.patch.material, ...path); - } - } - - /** @hidden */ - public breakReference(ref: Actor | Asset) { - if (!(ref instanceof Actor)) { return; } - if (ref.appearance.material === this) { - ref.appearance.material = null; - } - } -} +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { + Actor, + Asset, + AssetContainer, + AssetLike, + Color3, + Color4, + Color4Like, + Guid, + Vector2, + Vector2Like, + ZeroGuid +} from '..'; +import { + observe, + Patchable, + readPath +} from '../internal'; +import { AssetInternal } from './assetInternal'; + +/** + * Describes the properties of a Material. + */ +export interface MaterialLike { + /** The base color of this material. */ + color: Partial; + /** The main (albedo) texture asset ID */ + mainTextureId: Guid; + /** The main texture's offset from default */ + mainTextureOffset: Vector2Like; + /** The main texture's scale from default */ + mainTextureScale: Vector2Like; + /** How the color/texture's alpha channel should be handled */ + alphaMode: AlphaMode; + /** Visibility threshold in masked alpha mode */ + alphaCutoff: number; +} + +/** + * Controls how transparency is handled. + */ +export enum AlphaMode { + /** The object is rendered opaque, and transparency info is discarded. */ + Opaque = 'opaque', + /** + * Any parts with alpha above a certain cutoff ([[Material.alphaCutoff]]) + * will be rendered solid. Everything else is fully transparent. + */ + Mask = 'mask', + /** + * A pixel's transparency is directly proportional to its alpha value. + */ + Blend = 'blend' +} + +/** + * Represents a material on a mesh. + */ +export class Material extends Asset implements MaterialLike, Patchable { + private _color = Color4.FromColor3(Color3.White(), 1.0); + private _mainTextureId = ZeroGuid; + private _mainTextureOffset = Vector2.Zero(); + private _mainTextureScale = Vector2.One(); + private _alphaMode = AlphaMode.Opaque; + private _alphaCutoff = 0.5; + private _internal = new AssetInternal(this); + + /** @hidden */ + public get internal() { return this._internal; } + + /** @inheritdoc */ + public get color() { return this._color; } + public set color(value) { if (value) { this._color.copy(value); } } + + /** @returns A shared reference to this material's texture asset */ + public get mainTexture() { + return this.container.context.internal.lookupAsset(this._mainTextureId)?.texture; + } + public set mainTexture(value) { + this.mainTextureId = value?.id ?? ZeroGuid; + } + + /** @inheritdoc */ + public get mainTextureId() { return this._mainTextureId; } + public set mainTextureId(value) { + if (!value) { + value = ZeroGuid; + } + if (!this.container.context.internal.lookupAsset(value)) { + value = ZeroGuid; // throw? + } + + if (value === this._mainTextureId) { return; } + + if (this.mainTexture) { + this.mainTexture.clearReference(this); + } + this._mainTextureId = value; + if (this.mainTexture) { + this.mainTexture.addReference(this); + } + this.materialChanged('mainTextureId'); + } + + /** @inheritdoc */ + public get mainTextureOffset() { return this._mainTextureOffset; } + public set mainTextureOffset(value) { if (value) { this._mainTextureOffset.copy(value); } } + + /** @inheritdoc */ + public get mainTextureScale() { return this._mainTextureScale; } + public set mainTextureScale(value) { if (value) { this._mainTextureScale.copy(value); } } + + /** @inheritdoc */ + public get alphaMode() { return this._alphaMode; } + public set alphaMode(value) { this._alphaMode = value; this.materialChanged('alphaMode'); } + + /** @inheritdoc */ + public get alphaCutoff() { return this._alphaCutoff; } + public set alphaCutoff(value) { this._alphaCutoff = value; this.materialChanged('alphaCutoff'); } + + /** @inheritdoc */ + public get material(): Material { return this; } + + /** INTERNAL USE ONLY. To create a new material from scratch, use [[AssetManager.createMaterial]]. */ + public constructor(container: AssetContainer, def: AssetLike) { + super(container, def); + + if (!def.material) { + throw new Error("Cannot construct material from non-material definition"); + } + + this.copy(def); + + // material patching: observe the nested material properties + // for changed values, and write them to a patch + observe({ + target: this._color, + targetName: 'color', + notifyChanged: (...path: string[]) => this.materialChanged(...path) + }); + observe({ + target: this._mainTextureOffset, + targetName: 'mainTextureOffset', + notifyChanged: (...path: string[]) => this.materialChanged(...path) + }); + observe({ + target: this._mainTextureScale, + targetName: 'mainTextureScale', + notifyChanged: (...path: string[]) => this.materialChanged(...path) + }); + } + + public copy(from: Partial): this { + if (!from) { + return this; + } + + // Pause change detection while we copy the values into the actor. + const wasObserving = this.internal.observing; + this.internal.observing = false; + + super.copy(from); + if (from.material) { + if (from.material.color) { + this.color.copy(from.material.color); + } + if (from.material.mainTextureOffset) { + this.mainTextureOffset.copy(from.material.mainTextureOffset); + } + if (from.material.mainTextureScale) { + this.mainTextureScale.copy(from.material.mainTextureScale); + } + if (from.material.mainTextureId) { + this.mainTextureId = from.material.mainTextureId; + } + if (from.material.alphaMode) { + this.alphaMode = from.material.alphaMode; + } + if (from.material.alphaCutoff) { + this.alphaCutoff = from.material.alphaCutoff; + } + } + + this.internal.observing = wasObserving; + return this; + } + + /** @hidden */ + public toJSON(): AssetLike { + return { + ...super.toJSON(), + material: { + color: this.color.toJSON(), + mainTextureId: this.mainTextureId, + mainTextureOffset: this.mainTextureOffset.toJSON(), + mainTextureScale: this.mainTextureScale.toJSON(), + alphaMode: this.alphaMode, + alphaCutoff: this.alphaCutoff + } + }; + } + + private materialChanged(...path: string[]): void { + if (this.internal.observing) { + this.container.context.internal.incrementGeneration(); + this.internal.patch = this.internal.patch || { material: {} } as AssetLike; + readPath(this, this.internal.patch.material, ...path); + } + } + + /** @hidden */ + public breakReference(ref: Actor | Asset) { + if (!(ref instanceof Actor)) { return; } + if (ref.appearance.material === this) { + ref.appearance.material = null; + } + } +} diff --git a/packages/sdk/src/types/runtime/assets/mesh.ts b/packages/sdk/src/asset/mesh.ts similarity index 88% rename from packages/sdk/src/types/runtime/assets/mesh.ts rename to packages/sdk/src/asset/mesh.ts index 1046e86e8..3ffaa0222 100644 --- a/packages/sdk/src/types/runtime/assets/mesh.ts +++ b/packages/sdk/src/asset/mesh.ts @@ -1,106 +1,112 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -import { Asset, AssetContainer, AssetLike } from '.'; -import { Actor } from '..'; -import { PrimitiveDefinition, Vector3, Vector3Like } from '../../..'; -import { InternalAsset } from '../../internal/asset'; -import { Patchable } from '../../patchable'; - -/** Describes the properties of a mesh */ -export interface MeshLike { - /** The number of vertices in this mesh. */ - vertexCount: number; - /** The number of triangles in this mesh. */ - triangleCount: number; - /** The size of the axis-aligned box that exactly contains the mesh */ - boundingBoxDimensions: Vector3Like; - /** The center of the axis-aligned box that exactly contains the mesh */ - boundingBoxCenter: Vector3Like; - - /** If this mesh is a primitive, the primitive's description */ - primitiveDefinition: PrimitiveDefinition; -} - -/** Represents a mesh on an actor */ -export class Mesh extends Asset implements MeshLike, Patchable { - private _internal = new InternalAsset(this); - private _vertexCount: number; - private _triangleCount: number; - private _dimensions: Vector3 = new Vector3(); - private _center: Vector3 = new Vector3(); - private _primDef: PrimitiveDefinition = null; - - /** @hidden */ - public get internal() { return this._internal; } - - /** @inheritdoc */ - public get vertexCount() { return this._vertexCount; } - /** @inheritdoc */ - public get triangleCount() { return this._triangleCount; } - /** @inheritdoc */ - public get boundingBoxDimensions() { return this._dimensions; } - /** @inheritdoc */ - public get boundingBoxCenter() { return this._center; } - /** @inheritdoc */ - public get primitiveDefinition() { return this._primDef; } - - /** @inheritdoc */ - public get mesh(): Mesh { return this; } - - /** @hidden */ - public constructor(container: AssetContainer, def: AssetLike) { - super(container, def); - - if (!def.mesh) { - throw new Error("Cannot construct mesh from non-mesh definition"); - } - - this.copy(def); - } - - public copy(from: Partial): this { - if (!from) { - return this; - } - - // Pause change detection while we copy the values into the actor. - const wasObserving = this.internal.observing; - this.internal.observing = false; - - super.copy(from); - if (from.mesh?.vertexCount !== undefined) { this._vertexCount = from.mesh.vertexCount; } - if (from.mesh?.triangleCount !== undefined) { this._triangleCount = from.mesh.triangleCount; } - if (from.mesh?.boundingBoxDimensions) { this._dimensions.copy(from.mesh.boundingBoxDimensions); } - if (from.mesh?.boundingBoxCenter) { this._center.copy(from.mesh.boundingBoxCenter); } - if (from.mesh?.primitiveDefinition) { this._primDef = from.mesh.primitiveDefinition; } - - this.internal.observing = wasObserving; - return this; - } - - /** @hidden */ - public toJSON(): AssetLike { - return { - ...super.toJSON(), - mesh: { - vertexCount: this.vertexCount, - triangleCount: this.triangleCount, - boundingBoxDimensions: this.boundingBoxDimensions, - boundingBoxCenter: this.boundingBoxCenter, - primitiveDefinition: this.primitiveDefinition - } - }; - } - - /** @hidden */ - public breakReference(ref: Actor | Asset) { - if (!(ref instanceof Actor)) { return; } - - if (ref.appearance.mesh === this) { - ref.appearance.mesh = null; - } - } -} +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { + Actor, + Asset, + AssetContainer, + AssetLike, + PrimitiveDefinition, + Vector3, + Vector3Like, +} from '..'; +import { Patchable } from '../internal'; +import { AssetInternal } from './assetInternal'; + +/** Describes the properties of a mesh */ +export interface MeshLike { + /** The number of vertices in this mesh. */ + vertexCount: number; + /** The number of triangles in this mesh. */ + triangleCount: number; + /** The size of the axis-aligned box that exactly contains the mesh */ + boundingBoxDimensions: Vector3Like; + /** The center of the axis-aligned box that exactly contains the mesh */ + boundingBoxCenter: Vector3Like; + + /** If this mesh is a primitive, the primitive's description */ + primitiveDefinition: PrimitiveDefinition; +} + +/** Represents a mesh on an actor */ +export class Mesh extends Asset implements MeshLike, Patchable { + private _internal = new AssetInternal(this); + private _vertexCount: number; + private _triangleCount: number; + private _dimensions: Vector3 = new Vector3(); + private _center: Vector3 = new Vector3(); + private _primDef: PrimitiveDefinition = null; + + /** @hidden */ + public get internal() { return this._internal; } + + /** @inheritdoc */ + public get vertexCount() { return this._vertexCount; } + /** @inheritdoc */ + public get triangleCount() { return this._triangleCount; } + /** @inheritdoc */ + public get boundingBoxDimensions() { return this._dimensions; } + /** @inheritdoc */ + public get boundingBoxCenter() { return this._center; } + /** @inheritdoc */ + public get primitiveDefinition() { return this._primDef; } + + /** @inheritdoc */ + public get mesh(): Mesh { return this; } + + /** @hidden */ + public constructor(container: AssetContainer, def: AssetLike) { + super(container, def); + + if (!def.mesh) { + throw new Error("Cannot construct mesh from non-mesh definition"); + } + + this.copy(def); + } + + public copy(from: Partial): this { + if (!from) { + return this; + } + + // Pause change detection while we copy the values into the actor. + const wasObserving = this.internal.observing; + this.internal.observing = false; + + super.copy(from); + if (from.mesh?.vertexCount !== undefined) { this._vertexCount = from.mesh.vertexCount; } + if (from.mesh?.triangleCount !== undefined) { this._triangleCount = from.mesh.triangleCount; } + if (from.mesh?.boundingBoxDimensions) { this._dimensions.copy(from.mesh.boundingBoxDimensions); } + if (from.mesh?.boundingBoxCenter) { this._center.copy(from.mesh.boundingBoxCenter); } + if (from.mesh?.primitiveDefinition) { this._primDef = from.mesh.primitiveDefinition; } + + this.internal.observing = wasObserving; + return this; + } + + /** @hidden */ + public toJSON(): AssetLike { + return { + ...super.toJSON(), + mesh: { + vertexCount: this.vertexCount, + triangleCount: this.triangleCount, + boundingBoxDimensions: this.boundingBoxDimensions, + boundingBoxCenter: this.boundingBoxCenter, + primitiveDefinition: this.primitiveDefinition + } + }; + } + + /** @hidden */ + public breakReference(ref: Actor | Asset) { + if (!(ref instanceof Actor)) { return; } + + if (ref.appearance.mesh === this) { + ref.appearance.mesh = null; + } + } +} diff --git a/packages/sdk/src/types/runtime/assets/prefab.ts b/packages/sdk/src/asset/prefab.ts similarity index 82% rename from packages/sdk/src/types/runtime/assets/prefab.ts rename to packages/sdk/src/asset/prefab.ts index e443e31ba..f3daa42ef 100644 --- a/packages/sdk/src/types/runtime/assets/prefab.ts +++ b/packages/sdk/src/asset/prefab.ts @@ -1,72 +1,71 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -import { Asset, AssetContainer, AssetLike } from '.'; -import { Actor } from '..'; -import { InternalAsset } from '../../internal/asset'; -import { Patchable } from '../../patchable'; - -export interface PrefabLike { - /** The number of actors this prefab contains. */ - actorCount: number; -} - -export class Prefab extends Asset implements PrefabLike, Patchable { - private _actorCount: number; - private _internal = new InternalAsset(this); - - /** @hidden */ - public get internal() { return this._internal; } - - /** @inheritdoc */ - public get actorCount() { return this._actorCount; } - - /** @inheritdoc */ - public get prefab(): Prefab { return this; } - - /** @hidden */ - public constructor(container: AssetContainer, def: AssetLike) { - super(container, def); - - if (!def.prefab) { - throw new Error("Cannot construct prefab from non-prefab definition"); - } - - this.copy(def); - } - - public copy(from: Partial): this { - if (!from) { - return this; - } - - // Pause change detection while we copy the values into the actor. - const wasObserving = this.internal.observing; - this.internal.observing = false; - - super.copy(from); - if (from.prefab) { - this._actorCount = from.prefab.actorCount; - } - - this.internal.observing = wasObserving; - return this; - } - - /** @hidden */ - public toJSON(): AssetLike { - return { - ...super.toJSON(), - prefab: { - actorCount: this._actorCount - } - }; - } - - /** @hidden */ - public breakReference(ref: Actor | Asset) { - if (!(ref instanceof Actor)) { return; } - } -} +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { Actor, Asset, AssetContainer, AssetLike } from '..'; +import { Patchable } from '../internal'; +import { AssetInternal } from './assetInternal'; + +export interface PrefabLike { + /** The number of actors this prefab contains. */ + actorCount: number; +} + +export class Prefab extends Asset implements PrefabLike, Patchable { + private _actorCount: number; + private _internal = new AssetInternal(this); + + /** @hidden */ + public get internal() { return this._internal; } + + /** @inheritdoc */ + public get actorCount() { return this._actorCount; } + + /** @inheritdoc */ + public get prefab(): Prefab { return this; } + + /** @hidden */ + public constructor(container: AssetContainer, def: AssetLike) { + super(container, def); + + if (!def.prefab) { + throw new Error("Cannot construct prefab from non-prefab definition"); + } + + this.copy(def); + } + + public copy(from: Partial): this { + if (!from) { + return this; + } + + // Pause change detection while we copy the values into the actor. + const wasObserving = this.internal.observing; + this.internal.observing = false; + + super.copy(from); + if (from.prefab) { + this._actorCount = from.prefab.actorCount; + } + + this.internal.observing = wasObserving; + return this; + } + + /** @hidden */ + public toJSON(): AssetLike { + return { + ...super.toJSON(), + prefab: { + actorCount: this._actorCount + } + }; + } + + /** @hidden */ + public breakReference(ref: Actor | Asset) { + if (!(ref instanceof Actor)) { return; } + } +} diff --git a/packages/sdk/src/types/runtime/assets/sound.ts b/packages/sdk/src/asset/sound.ts similarity index 88% rename from packages/sdk/src/types/runtime/assets/sound.ts rename to packages/sdk/src/asset/sound.ts index f11cf7084..10ea14d05 100644 --- a/packages/sdk/src/types/runtime/assets/sound.ts +++ b/packages/sdk/src/asset/sound.ts @@ -3,10 +3,9 @@ * Licensed under the MIT License. */ -import { Asset, AssetContainer, AssetLike } from '.'; -import { Actor } from '..'; -import { InternalAsset } from '../../internal/asset'; -import { Patchable } from '../../patchable'; +import { Actor, Asset, AssetContainer, AssetLike } from '..'; +import { Patchable } from '../internal'; +import { AssetInternal } from './assetInternal'; export interface SoundLike { uri: string; @@ -16,7 +15,7 @@ export interface SoundLike { export class Sound extends Asset implements SoundLike, Patchable { private _uri: string; private _duration = 0; - private _internal = new InternalAsset(this); + private _internal = new AssetInternal(this); /** @hidden */ public get internal() { return this._internal; } diff --git a/packages/sdk/src/types/runtime/assets/texture.ts b/packages/sdk/src/asset/texture.ts similarity index 87% rename from packages/sdk/src/types/runtime/assets/texture.ts rename to packages/sdk/src/asset/texture.ts index 49ce4e34f..a8c2cba7a 100644 --- a/packages/sdk/src/types/runtime/assets/texture.ts +++ b/packages/sdk/src/asset/texture.ts @@ -1,123 +1,120 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -import { Asset, AssetContainer, AssetLike, Material } from '.'; -import { Actor } from '..'; -import { Vector2, Vector2Like } from '../../../math'; -import readPath from '../../../utils/readPath'; -import { InternalAsset } from '../../internal/asset'; -import { Patchable } from '../../patchable'; - -export interface TextureLike { - uri: string; - resolution: Vector2Like; - wrapU: TextureWrapMode; - wrapV: TextureWrapMode; -} - -/** How a material should interpret UV coordinates outside the [0,1) range. */ -export enum TextureWrapMode { - /** The texture is tiled for every 1 unit in the UVs. */ - Repeat = 'repeat', - /** The edge pixels of the texture are stretched out to the bounds of the UVs. */ - Clamp = 'clamp', - /** The texture is tiled and flipped for every 1 unit in the UVs. */ - Mirror = 'mirror' -} - -export class Texture extends Asset implements TextureLike, Patchable { - private _uri: string; - private _resolution = Vector2.One(); - private _wrapU = TextureWrapMode.Repeat; - private _wrapV = TextureWrapMode.Repeat; - private _internal = new InternalAsset(this); - - /** @hidden */ - public get internal() { return this._internal; } - - /** The URI, if any, this texture was loaded from */ - public get uri() { return this._uri; } - - /** The pixel dimensions of the loaded texture */ - public get resolution() { return this._resolution; } - - /** How overflowing UVs are handled horizontally. */ - public get wrapU() { return this._wrapU; } - public set wrapU(val) { this._wrapU = val; this.textureChanged('wrapU'); } - - /** How overflowing UVs are handled vertically. */ - public get wrapV() { return this._wrapV; } - public set wrapV(val) { this._wrapV = val; this.textureChanged('wrapV'); } - - /** @inheritdoc */ - public get texture(): Texture { return this; } - - /** INTERNAL USE ONLY. To load a new texture from scratch, use [[AssetManager.createTexture]] */ - public constructor(container: AssetContainer, def: AssetLike) { - super(container, def); - - if (!def.texture) { - throw new Error("Cannot construct texture from non-texture definition"); - } - - this.copy(def); - } - - public copy(from: Partial): this { - if (!from) { - return this; - } - - // Pause change detection while we copy the values into the actor. - const wasObserving = this.internal.observing; - this.internal.observing = false; - - super.copy(from); - if (from.texture && from.texture.uri) { - this._uri = from.texture.uri; - } - if (from.texture && from.texture.resolution) { - this._resolution = new Vector2(from.texture.resolution.x, from.texture.resolution.y); - } - if (from.texture && from.texture.wrapU) { - this.wrapU = from.texture.wrapU; - } - if (from.texture && from.texture.wrapV) { - this.wrapV = from.texture.wrapV; - } - - this.internal.observing = wasObserving; - return this; - } - - /** @hidden */ - public toJSON(): AssetLike { - return { - ...super.toJSON(), - texture: { - uri: this.uri, - resolution: this.resolution.toJSON(), - wrapU: this.wrapU, - wrapV: this.wrapV - } - }; - } - - private textureChanged(...path: string[]): void { - if (this.internal.observing) { - this.container.context.internal.incrementGeneration(); - this.internal.patch = this.internal.patch || { texture: {} } as AssetLike; - readPath(this, this.internal.patch.texture, ...path); - } - } - - /** @hidden */ - public breakReference(ref: Actor | Asset) { - if (!(ref instanceof Material)) { return; } - if (ref.mainTexture === this) { - ref.mainTexture = null; - } - } -} +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { Actor, Asset, AssetContainer, AssetLike, Material, Vector2, Vector2Like } from '..'; +import { Patchable, readPath } from '../internal'; +import { AssetInternal } from './assetInternal'; + +export interface TextureLike { + uri: string; + resolution: Vector2Like; + wrapU: TextureWrapMode; + wrapV: TextureWrapMode; +} + +/** How a material should interpret UV coordinates outside the [0,1) range. */ +export enum TextureWrapMode { + /** The texture is tiled for every 1 unit in the UVs. */ + Repeat = 'repeat', + /** The edge pixels of the texture are stretched out to the bounds of the UVs. */ + Clamp = 'clamp', + /** The texture is tiled and flipped for every 1 unit in the UVs. */ + Mirror = 'mirror' +} + +export class Texture extends Asset implements TextureLike, Patchable { + private _uri: string; + private _resolution = Vector2.One(); + private _wrapU = TextureWrapMode.Repeat; + private _wrapV = TextureWrapMode.Repeat; + private _internal = new AssetInternal(this); + + /** @hidden */ + public get internal() { return this._internal; } + + /** The URI, if any, this texture was loaded from */ + public get uri() { return this._uri; } + + /** The pixel dimensions of the loaded texture */ + public get resolution() { return this._resolution; } + + /** How overflowing UVs are handled horizontally. */ + public get wrapU() { return this._wrapU; } + public set wrapU(val) { this._wrapU = val; this.textureChanged('wrapU'); } + + /** How overflowing UVs are handled vertically. */ + public get wrapV() { return this._wrapV; } + public set wrapV(val) { this._wrapV = val; this.textureChanged('wrapV'); } + + /** @inheritdoc */ + public get texture(): Texture { return this; } + + /** INTERNAL USE ONLY. To load a new texture from scratch, use [[AssetManager.createTexture]] */ + public constructor(container: AssetContainer, def: AssetLike) { + super(container, def); + + if (!def.texture) { + throw new Error("Cannot construct texture from non-texture definition"); + } + + this.copy(def); + } + + public copy(from: Partial): this { + if (!from) { + return this; + } + + // Pause change detection while we copy the values into the actor. + const wasObserving = this.internal.observing; + this.internal.observing = false; + + super.copy(from); + if (from.texture && from.texture.uri) { + this._uri = from.texture.uri; + } + if (from.texture && from.texture.resolution) { + this._resolution = new Vector2(from.texture.resolution.x, from.texture.resolution.y); + } + if (from.texture && from.texture.wrapU) { + this.wrapU = from.texture.wrapU; + } + if (from.texture && from.texture.wrapV) { + this.wrapV = from.texture.wrapV; + } + + this.internal.observing = wasObserving; + return this; + } + + /** @hidden */ + public toJSON(): AssetLike { + return { + ...super.toJSON(), + texture: { + uri: this.uri, + resolution: this.resolution.toJSON(), + wrapU: this.wrapU, + wrapV: this.wrapV + } + }; + } + + private textureChanged(...path: string[]): void { + if (this.internal.observing) { + this.container.context.internal.incrementGeneration(); + this.internal.patch = this.internal.patch || { texture: {} } as AssetLike; + readPath(this, this.internal.patch.texture, ...path); + } + } + + /** @hidden */ + public breakReference(ref: Actor | Asset) { + if (!(ref instanceof Material)) { return; } + if (ref.mainTexture === this) { + ref.mainTexture = null; + } + } +} diff --git a/packages/sdk/src/types/runtime/assets/videoStream.ts b/packages/sdk/src/asset/videoStream.ts similarity index 89% rename from packages/sdk/src/types/runtime/assets/videoStream.ts rename to packages/sdk/src/asset/videoStream.ts index ae4c22a61..6d71e9ae9 100644 --- a/packages/sdk/src/types/runtime/assets/videoStream.ts +++ b/packages/sdk/src/asset/videoStream.ts @@ -3,10 +3,9 @@ * Licensed under the MIT License. */ -import { Asset, AssetContainer, AssetLike } from '.'; -import { Actor } from '..'; -import { InternalAsset } from '../../internal/asset'; -import { Patchable } from '../../patchable'; +import { Actor, Asset, AssetContainer, AssetLike } from '..'; +import { Patchable } from '../internal'; +import { AssetInternal } from './assetInternal'; export interface VideoStreamLike { uri: string; @@ -16,7 +15,7 @@ export interface VideoStreamLike { export class VideoStream extends Asset implements VideoStreamLike, Patchable { private _uri: string; private _duration = 0; - private _internal = new InternalAsset(this); + private _internal = new AssetInternal(this); /** @hidden */ public get internal() { return this._internal; } diff --git a/packages/sdk/src/types/runtime/context.ts b/packages/sdk/src/core/context.ts similarity index 89% rename from packages/sdk/src/types/runtime/context.ts rename to packages/sdk/src/core/context.ts index 51a8ffc41..d263403a6 100644 --- a/packages/sdk/src/types/runtime/context.ts +++ b/packages/sdk/src/core/context.ts @@ -1,182 +1,174 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -import events from 'events'; -import { - Actor, - Connection, - Guid, - newGuid, - NullConnection, - User, -} from '../..'; -import { RPC, RPCChannels } from '../../rpc'; -import { InternalContext } from '../internal/context'; -import * as Payloads from '../network/payloads'; - -/** - * Settings used to configure a `Context` instance. - */ -export interface ContextSettings { - connection?: Connection; - sessionId?: string; -} - -/** - * Container for an application session. The Context contains all application state for a session of your application. - * This includes Actors, Users, Assets, and other state. - */ -export class Context { - private _internal: InternalContext; - /** @hidden */ - public get internal() { return this._internal; } - - private _emitter = new events.EventEmitter(); - /** @hidden */ - public get emitter() { return this._emitter; } - - private _sessionId: string; - private _conn: Connection; - private _rpcChannels: RPCChannels; - private _rpc: RPC; - - public get sessionId() { return this._sessionId; } - public get conn() { return this._conn; } - public get actors() { return [...this.internal.actorSet.values()]; } - public get rootActors() { return this.actors.filter(a => !a.parent); } - public get users() { return [...this.internal.userSet.values()]; } - public get rpcChannels() { return this._rpcChannels; } - public get rpc() { return this._rpc; } - public actor = (actorId: Guid): Actor => this.internal.actorSet.get(actorId); - public user = (userId: Guid): User => this.internal.userSet.get(userId); - - /** - * Creates a new `Context` instance. - */ - constructor(settings: ContextSettings) { - this._conn = settings.connection || new NullConnection(); - this._sessionId = settings.sessionId || newGuid().toString(); - this._internal = new InternalContext(this); - this._rpcChannels = new RPCChannels(); - this._rpc = new RPC(this); - this.rpcChannels.setChannelHandler(null, this._rpc); - } - - /** - * Exits this context. - */ - public quit() { - // Closing the connection triggers events that will tear down the context. - this.conn.close(); - } - - /** - * The onStarted event is raised after the Context is fully initialized and ready for your application logic to - * start executing. - * @event - */ - public onStarted(handler: () => void): this { - this.emitter.addListener('started', handler); - return this; - } - - /** - * The onStopped event is raised before the Context starts shutting down, which happens after the last user - * disconnects. - * @event - */ - public onStopped(handler: () => void): this { - this.emitter.addListener('stopped', handler); - return this; - } - - /** - * The onUserJoined event is raised after a new user has joined the Context. - * @event - */ - public onUserJoined(handler: (user: User) => void): this { - this.emitter.addListener('user-joined', handler); - return this; - } - - /** - * Remove the onUserJoined event handler from the Context. - * @event - */ - public offUserJoined(handler: (user: User) => void): this { - this.emitter.removeListener('user-joined', handler); - return this; - } - - /** - * The onUserLeft event is raised when the given user has left the Context. After the last user leaves, the Context - * will be shutdown (and a 'stopped' event will soon follow). - * @event - */ - public onUserLeft(handler: (user: User) => void): this { - this.emitter.addListener('user-left', handler); - return this; - } - - /** - * Remove the onUserLeft event handler from the Context - * @event - */ - public offUserLeft(handler: (user: User) => void): this { - this.emitter.removeListener('user-left', handler); - return this; - } - - /** - * @hidden - * (for now) - */ - public onActorCreated(handler: (actor: Actor) => void): this { - this.emitter.addListener('actor-created', handler); - return this; - } - - /** - * @hidden - * (for now) - */ - public offActorCreated(handler: (actor: Actor) => void): this { - this.emitter.removeListener('actor-created', handler); - return this; - } - - /** - * @hidden - * (for now) - */ - public onActorDestroyed(handler: (actor: Actor) => void): this { - this.emitter.addListener('actor-destroyed', handler); - return this; - } - - /** - * @hidden - * (for now) - */ - public offActorDestroyed(handler: (actor: Actor) => void): this { - this.emitter.removeListener('actor-destroyed', handler); - return this; - } - - /** - * @hidden - */ - public receiveRPC(payload: Payloads.EngineToAppRPC) { - this.rpcChannels.receive(payload); - } - - /** - * Collect and return a snapshot of the current resource usage of the MRE subsystem. For Node process stats, - * use `process.resourceUsage()`. - */ - public getStats() { - return this.internal.getStats(); - } -} +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import events from 'events'; +import { Actor, Guid, newGuid, RPC, RPCChannels, User, } from '..'; +import { Connection, NullConnection, Payloads } from '../internal'; +import { ContextInternal } from './contextInternal'; + +/** + * Settings used to configure a `Context` instance. + */ +export interface ContextSettings { + connection?: Connection; + sessionId?: string; +} + +/** + * Container for an application session. The Context contains all application state for a session of your application. + * This includes Actors, Users, Assets, and other state. + */ +export class Context { + private _internal: ContextInternal; + /** @hidden */ + public get internal() { return this._internal; } + + private _emitter = new events.EventEmitter(); + /** @hidden */ + public get emitter() { return this._emitter; } + + private _sessionId: string; + private _conn: Connection; + private _rpcChannels: RPCChannels; + private _rpc: RPC; + + public get sessionId() { return this._sessionId; } + public get conn() { return this._conn; } + public get actors() { return [...this.internal.actorSet.values()]; } + public get rootActors() { return this.actors.filter(a => !a.parent); } + public get users() { return [...this.internal.userSet.values()]; } + public get rpcChannels() { return this._rpcChannels; } + public get rpc() { return this._rpc; } + public actor = (actorId: Guid): Actor => this.internal.actorSet.get(actorId); + public user = (userId: Guid): User => this.internal.userSet.get(userId); + + /** + * Creates a new `Context` instance. + */ + constructor(settings: ContextSettings) { + this._conn = settings.connection || new NullConnection(); + this._sessionId = settings.sessionId || newGuid().toString(); + this._internal = new ContextInternal(this); + this._rpcChannels = new RPCChannels(); + this._rpc = new RPC(this); + this.rpcChannels.setChannelHandler(null, this._rpc); + } + + /** + * Exits this context. + */ + public quit() { + // Closing the connection triggers events that will tear down the context. + this.conn.close(); + } + + /** + * The onStarted event is raised after the Context is fully initialized and ready for your application logic to + * start executing. + * @event + */ + public onStarted(handler: () => void): this { + this.emitter.addListener('started', handler); + return this; + } + + /** + * The onStopped event is raised before the Context starts shutting down, which happens after the last user + * disconnects. + * @event + */ + public onStopped(handler: () => void): this { + this.emitter.addListener('stopped', handler); + return this; + } + + /** + * The onUserJoined event is raised after a new user has joined the Context. + * @event + */ + public onUserJoined(handler: (user: User) => void): this { + this.emitter.addListener('user-joined', handler); + return this; + } + + /** + * Remove the onUserJoined event handler from the Context. + * @event + */ + public offUserJoined(handler: (user: User) => void): this { + this.emitter.removeListener('user-joined', handler); + return this; + } + + /** + * The onUserLeft event is raised when the given user has left the Context. After the last user leaves, the Context + * will be shutdown (and a 'stopped' event will soon follow). + * @event + */ + public onUserLeft(handler: (user: User) => void): this { + this.emitter.addListener('user-left', handler); + return this; + } + + /** + * Remove the onUserLeft event handler from the Context + * @event + */ + public offUserLeft(handler: (user: User) => void): this { + this.emitter.removeListener('user-left', handler); + return this; + } + + /** + * @hidden + * (for now) + */ + public onActorCreated(handler: (actor: Actor) => void): this { + this.emitter.addListener('actor-created', handler); + return this; + } + + /** + * @hidden + * (for now) + */ + public offActorCreated(handler: (actor: Actor) => void): this { + this.emitter.removeListener('actor-created', handler); + return this; + } + + /** + * @hidden + * (for now) + */ + public onActorDestroyed(handler: (actor: Actor) => void): this { + this.emitter.addListener('actor-destroyed', handler); + return this; + } + + /** + * @hidden + * (for now) + */ + public offActorDestroyed(handler: (actor: Actor) => void): this { + this.emitter.removeListener('actor-destroyed', handler); + return this; + } + + /** + * @hidden + */ + public receiveRPC(payload: Payloads.EngineToAppRPC) { + this.rpcChannels.receive(payload); + } + + /** + * Collect and return a snapshot of the current resource usage of the MRE subsystem. For Node process stats, + * use `process.resourceUsage()`. + */ + public getStats() { + return this.internal.getStats(); + } +} diff --git a/packages/sdk/src/types/internal/context.ts b/packages/sdk/src/core/contextInternal.ts similarity index 92% rename from packages/sdk/src/types/internal/context.ts rename to packages/sdk/src/core/contextInternal.ts index 3c9ce3b30..abe05228e 100644 --- a/packages/sdk/src/types/internal/context.ts +++ b/packages/sdk/src/core/contextInternal.ts @@ -1,687 +1,685 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ -import { - ActionEvent, - Actor, - ActorLike, - Animation, - AnimationLike, - AnimationWrapMode, - Asset, - AssetContainer, - AssetContainerIterable, - AssetLike, - BehaviorType, - CollisionEvent, - CollisionLayer, - Context, - CreateAnimationOptions, - Guid, - MediaCommand, - newGuid, - PerformanceStats, - SetAnimationStateOptions, - SetMediaStateOptions, - TriggerEvent, - User, - UserLike, - ZeroGuid, -} from '../..'; - -import * as Payloads from '../network/payloads'; - -import { log } from '../../log'; -import * as Protocols from '../../protocols'; -import { Execution } from '../../protocols/execution'; -import { Handshake } from '../../protocols/handshake'; -import { ExportedPromise } from '../../utils/exportedPromise'; -import resolveJsonValues from '../../utils/resolveJsonValues'; -import safeGet from '../../utils/safeAccessPath'; -import { OperatingModel } from '../network/operatingModel'; -import { Patchable } from '../patchable'; -import { MediaInstance } from '../runtime/mediaInstance'; - -/** - * @hidden - */ -export class InternalContext { - public actorSet = new Map(); - public userSet = new Map(); - public userGroupMapping: { [id: string]: number } = { default: 1 }; - public assetContainers = new Set(); - public animationSet: Map = new Map(); - public protocol: Protocols.Protocol; - public interval: NodeJS.Timer; - public generation = 0; - public prevGeneration = 0; - public __rpc: any; - - constructor(public context: Context) { - // Handle connection close events. - this.onClose = this.onClose.bind(this); - this.context.conn.on('close', this.onClose); - } - - public Create(options?: { - actor?: Partial; - }): Actor { - return this.createActorFromPayload({ - ...options, - actor: { - ...(options && options.actor), - id: newGuid() - }, - type: 'create-empty' - } as Payloads.CreateEmpty); - } - - public CreateFromLibrary(options: { - resourceId: string; - actor?: Partial; - }): Actor { - return this.createActorFromPayload({ - ...options, - actor: { - ...(options && options.actor), - id: newGuid() - }, - type: 'create-from-library' - } as Payloads.CreateFromLibrary); - } - - public CreateFromPrefab(options: { - prefabId: Guid; - collisionLayer?: CollisionLayer; - actor?: Partial; - }): Actor { - return this.createActorFromPayload({ - ...options, - actor: { - ...(options && options.actor), - id: newGuid() - }, - type: 'create-from-prefab' - } as Payloads.CreateFromPrefab); - } - - private createActorFromPayload( - payload: Payloads.CreateActorCommon - ): Actor { - // Resolve by-reference values now, ensuring they won't change in the - // time between now and when this message is actually sent. - payload.actor = Actor.sanitize(payload.actor); - // Create the actor locally. - this.updateActors(payload.actor); - // Get a reference to the new actor. - const actor = this.context.actor(payload.actor.id); - - this.protocol.sendPayload( payload, { - resolve: (replyPayload: Payloads.ObjectSpawned | Payloads.OperationResult) => { - this.protocol.recvPayload(replyPayload); - let success: boolean; - let message: string; - if (replyPayload.type === 'operation-result') { - success = replyPayload.resultCode !== 'error'; - message = replyPayload.message; - } else { - success = replyPayload.result.resultCode !== 'error'; - message = replyPayload.result.message; - - for (const createdAnimLike of replyPayload.animations) { - if (!this.animationSet.has(createdAnimLike.id)) { - const createdAnim = new Animation(this.context, createdAnimLike.id); - createdAnim.copy(createdAnimLike); - this.animationSet.set(createdAnimLike.id, createdAnim); - } - } - - for (const createdActorLike of replyPayload.actors) { - const createdActor = this.actorSet.get(createdActorLike.id); - if (createdActor) { - createdActor.internal.notifyCreated(success, replyPayload.result.message); - } - } - } - - if (success) { - if (!actor.collider && actor.internal.behavior) { - log.warning('app', 'Behaviors will not function on Unity host apps without adding a' - + ' collider to this actor first. Recommend adding a primitive collider' - + ' to this actor.'); - } - actor.internal.notifyCreated(true); - } else { - actor.internal.notifyCreated(false, message); - } - }, - reject: (reason?: any) => { - actor.internal.notifyCreated(false, reason); - } - }); - - return actor; - } - - public CreateFromGltf(container: AssetContainer, options: { - uri: string; - colliderType?: 'box' | 'mesh'; - actor?: Partial; - }): Actor { - // create actor locally - options.actor = Actor.sanitize({ ...options.actor, id: newGuid() }); - this.updateActors(options.actor); - const actor = this.context.actor(options.actor.id); - - // reserve actor so the pending actor is ready for commands - this.protocol.sendPayload({ - type: 'x-reserve-actor', - actor: options.actor - } as Payloads.XReserveActor); - - // kick off asset loading - container.loadGltf(options.uri, options.colliderType) - .then(assets => { - if (!this.context.actor(actor.id)) { - // actor was destroyed, stop loading - return; - } - - // once assets are done, find first prefab... - const prefab = assets.find(a => !!a.prefab); - if (!prefab) { - actor.internal.notifyCreated(false, `glTF contains no prefabs: ${options.uri}`); - return; - } - - // ...and spawn it - this.createActorFromPayload({ - type: 'create-from-prefab', - prefabId: prefab.id, - actor: options.actor - } as Payloads.CreateFromPrefab); - }) - .catch(reason => actor.internal.notifyCreated(false, reason)); - - return actor; - } - - public createAnimation(actorId: Guid, animationName: string, options: CreateAnimationOptions) { - const actor = this.actorSet.get(actorId); - if (!actor) { - log.error('app', `Failed to create animation on ${animationName}. Actor ${actorId} not found.`); - } - options = { - wrapMode: AnimationWrapMode.Once, - ...options - }; - - // Transform animations must be specified in local space - for (const frame of options.keyframes) { - if (frame.value.transform && !safeGet(frame.value, 'transform', 'local')) { - throw new Error("Transform animations must be specified in local space"); - } - } - - // generate the anim immediately - const createdAnim = new Animation(this.context, newGuid()); - createdAnim.copy({ - name: animationName, - targetActorIds: [actorId], - weight: options.initialState?.enabled === true ? 1 : 0, - speed: options.initialState?.speed, - time: options.initialState?.time - }); - this.animationSet.set(createdAnim.id, createdAnim); - - // Resolve by-reference values now, ensuring they won't change in the - // time between now and when this message is actually sent. - options.keyframes = resolveJsonValues(options.keyframes); - return new Promise((resolve, reject) => { - this.protocol.sendPayload({ - type: 'create-animation', - actorId, - animationName, - animationId: createdAnim.id.toString(), - ...options - } as Payloads.CreateAnimation, - { - resolve: (reply: Payloads.ObjectSpawned) => { - if (reply.result.resultCode !== 'error') { - createdAnim.copy(reply.animations[0]); - resolve(createdAnim); - } else { - reject(reply.result.message); - } - }, - reject - }); - }); - } - - public setAnimationState( - actorId: Guid, - animationName: string, - state: SetAnimationStateOptions - ) { - const actor = this.actorSet.get(actorId); - if (!actor) { - log.error('app', `Failed to set animation state on "${animationName}". Actor "${actorId}" not found.`); - return; - } - const anim = actor.animationsByName.get(animationName); - if (!anim) { - log.error('app', `Failed to set animation state on "${animationName}". ` + - `No animation with this name was found on actor "${actorId}" (${actor.name}).`); - return; - } - if (state.enabled !== undefined) { - anim.isPlaying = state.enabled; - } - if (state.speed !== undefined) { - anim.speed = state.speed; - } - if (state.time !== undefined) { - anim.time = state.time; - } - } - - public setMediaState( - mediaInstance: MediaInstance, - command: MediaCommand, - options?: SetMediaStateOptions, - mediaAssetId?: Guid, - ) { - this.protocol.sendPayload({ - type: 'set-media-state', - id: mediaInstance.id, - actorId: mediaInstance.actor.id, - mediaAssetId, - mediaCommand: command, - options - } as Payloads.SetMediaState); - } - - public animateTo( - actorId: Guid, - value: Partial, - duration: number, - curve: number[], - ) { - const actor = this.actorSet.get(actorId); - if (!actor) { - log.error('app', `Failed animateTo. Actor ${actorId} not found.`); - } else if (!Array.isArray(curve) || curve.length !== 4) { - log.error('app', '`curve` parameter must be an array of four numbers. ' + - 'Try passing one of the predefined curves from `AnimationEaseCurves`'); - } else { - this.protocol.sendPayload({ - type: 'interpolate-actor', - actorId, - animationName: newGuid().toString(), - value, - duration, - curve, - enabled: true - } as Payloads.InterpolateActor); - } - } - - public async startListening() { - try { - // Startup the handshake protocol. - const handshake = this.protocol = - new Handshake(this.context.conn, this.context.sessionId, OperatingModel.ServerAuthoritative); - await handshake.run(); - - // Switch to execution protocol. - const execution = this.protocol = new Execution(this.context); - - execution.on('protocol.update-actors', this.updateActors.bind(this)); - execution.on('protocol.destroy-actors', this.localDestroyActors.bind(this)); - execution.on('protocol.user-joined', this.userJoined.bind(this)); - execution.on('protocol.user-left', this.userLeft.bind(this)); - execution.on('protocol.update-user', this.updateUser.bind(this)); - execution.on('protocol.perform-action', this.performAction.bind(this)); - execution.on('protocol.receive-rpc', this.receiveRPC.bind(this)); - execution.on('protocol.collision-event-raised', this.collisionEventRaised.bind(this)); - execution.on('protocol.trigger-event-raised', this.triggerEventRaised.bind(this)); - execution.on('protocol.set-animation-state', this.setAnimationStateEventRaised.bind(this)); - execution.on('protocol.update-animations', this.updateAnimations.bind(this)); - - // Startup the execution protocol - execution.startListening(); - } catch (e) { - log.error('app', e); - } - } - - public start() { - if (!this.interval) { - this.interval = setInterval(() => this.update(), 0); - this.context.emitter.emit('started'); - } - } - - public stop() { - try { - if (this.interval) { - this.protocol.stopListening(); - clearInterval(this.interval); - this.interval = undefined; - this.context.emitter.emit('stopped'); - this.context.emitter.removeAllListeners(); - } - } catch { } - } - - public incrementGeneration() { - this.generation++; - } - - private assetsIterable() { - return new AssetContainerIterable([...this.assetContainers]); - } - - public update() { - // Early out if no state changes occurred. - if (this.generation === this.prevGeneration) { - return; - } - - this.prevGeneration = this.generation; - - const syncObjects = [ - ...this.actorSet.values(), - ...this.assetsIterable(), - ...this.userSet.values(), - ...this.animationSet.values() - ] as Array>; - - for (const patchable of syncObjects) { - const patch = patchable.internal.getPatchAndReset(); - if (!patch) { - continue; - } - - if (patchable instanceof Actor) { - this.protocol.sendPayload({ - type: 'actor-update', - actor: patch as ActorLike - } as Payloads.ActorUpdate); - } else if (patchable instanceof Animation) { - this.protocol.sendPayload({ - type: 'animation-update', - animation: patch as Partial - } as Payloads.AnimationUpdate) - } else if (patchable instanceof Asset) { - this.protocol.sendPayload({ - type: 'asset-update', - asset: patch as AssetLike - } as Payloads.AssetUpdate); - } else if (patchable instanceof User) { - this.protocol.sendPayload({ - type: 'user-update', - user: patch as UserLike - } as Payloads.UserUpdate); - } - } - - if (this.nextUpdatePromise) { - this.resolveNextUpdatePromise(); - this.nextUpdatePromise = null; - this.resolveNextUpdatePromise = null; - } - } - - private nextUpdatePromise: Promise; - private resolveNextUpdatePromise: () => void; - /** @hidden */ - public nextUpdate(): Promise { - if (this.nextUpdatePromise) { - return this.nextUpdatePromise; - } - - return this.nextUpdatePromise = new Promise(resolve => { - this.resolveNextUpdatePromise = resolve; - }); - } - - public sendDestroyActors(actorIds: Guid[]) { - if (actorIds.length) { - this.protocol.sendPayload({ - type: 'destroy-actors', - actorIds, - } as Payloads.DestroyActors); - } - } - - public updateActors(sactors: Partial | Array>) { - if (!sactors) { - return; - } - if (!Array.isArray(sactors)) { - sactors = [sactors]; - } - const newActorIds: Guid[] = []; - // Instantiate and store each actor. - sactors.forEach(sactor => { - const isNewActor = !this.actorSet.get(sactor.id); - const actor = isNewActor ? Actor.alloc(this.context, sactor.id) : this.actorSet.get(sactor.id); - this.actorSet.set(sactor.id, actor); - actor.copy(sactor); - if (isNewActor) { - newActorIds.push(actor.id); - } - }); - newActorIds.forEach(actorId => { - const actor = this.actorSet.get(actorId); - this.context.emitter.emit('actor-created', actor); - }); - } - - public updateAnimations(animPatches: Array>) { - if (!animPatches) { return; } - const newAnims: Animation[] = []; - for (const patch of animPatches) { - if (this.animationSet.has(patch.id)) { continue; } - const newAnim = new Animation(this.context, patch.id); - this.animationSet.set(newAnim.id, newAnim); - newAnim.copy(patch); - newAnims.push(newAnim); - } - for (const anim of newAnims) { - this.context.emitter.emit('animation-created', anim); - } - } - - public sendPayload(payload: Payloads.Payload, promise?: ExportedPromise): void { - this.protocol.sendPayload(payload, promise); - } - - public receiveRPC(payload: Payloads.EngineToAppRPC) { - this.context.receiveRPC(payload); - } - - public onClose = () => { - this.stop(); - }; - - public userJoined(suser: Partial) { - if (!this.userSet.has(suser.id)) { - const user = new User(this.context, suser.id); - this.userSet.set(suser.id, user); - user.copy(suser); - this.context.emitter.emit('user-joined', user); - } - } - - public userLeft(userId: Guid) { - const user = this.userSet.get(userId); - if (user) { - this.userSet.delete(userId); - this.context.emitter.emit('user-left', user); - } - } - - public updateUser(suser: Partial) { - let user = this.userSet.get(suser.id); - if (!user) { - user = new User(this.context, suser.id); - user.copy(suser); - this.userSet.set(user.id, user); - this.context.emitter.emit('user-joined', user); - } else { - user.copy(suser); - this.context.emitter.emit('user-updated', user); - } - } - - public performAction(actionEvent: ActionEvent) { - if (actionEvent.user) { - const targetActor = this.actorSet.get(actionEvent.targetId); - if (targetActor) { - targetActor.internal.performAction(actionEvent); - } - } - } - - public collisionEventRaised(collisionEvent: CollisionEvent) { - const actor = this.actorSet.get(collisionEvent.colliderOwnerId); - const otherActor = this.actorSet.get((collisionEvent.collisionData.otherActorId)); - if (actor && otherActor) { - // Update the collision data to contain the actual other actor. - collisionEvent.collisionData = { - ...collisionEvent.collisionData, - otherActor - }; - - actor.internal.collisionEventRaised( - collisionEvent.eventType, - collisionEvent.collisionData); - } - } - - public triggerEventRaised(triggerEvent: TriggerEvent) { - const actor = this.actorSet.get(triggerEvent.colliderOwnerId); - const otherActor = this.actorSet.get(triggerEvent.otherColliderOwnerId); - if (actor && otherActor) { - actor.internal.triggerEventRaised( - triggerEvent.eventType, - otherActor); - } - } - - public setAnimationStateEventRaised(actorId: Guid, animationName: string, state: SetAnimationStateOptions) { - const actor = this.context.actor(actorId); - if (actor) { - actor.internal.setAnimationStateEventRaised(animationName, state); - } - } - - public localDestroyActors(actorIds: Guid[]) { - for (const actorId of actorIds) { - if (this.actorSet.has(actorId)) { - this.localDestroyActor(this.actorSet.get(actorId)); - } - } - } - - public localDestroyActor(actor: Actor) { - // Recursively destroy children first - (actor.children || []).forEach(child => { - this.localDestroyActor(child); - }); - // Remove actor from _actors - this.actorSet.delete(actor.id); - // Raise event - this.context.emitter.emit('actor-destroyed', actor); - } - - public destroyActor(actorId: Guid) { - const actor = this.actorSet.get(actorId); - if (actor) { - // Tell engine to destroy the actor (will destroy all children too) - this.sendDestroyActors([actorId]); - // Clean up the actor locally - this.localDestroyActor(actor); - } - } - - public sendRigidBodyCommand(actorId: Guid, payload: Payloads.Payload) { - this.protocol.sendPayload({ - type: 'rigidbody-commands', - actorId, - commandPayloads: [payload] - } as Payloads.RigidBodyCommands); - } - - public setBehavior(actorId: Guid, newBehaviorType: BehaviorType) { - const actor = this.actorSet.get(actorId); - if (actor) { - this.protocol.sendPayload({ - type: 'set-behavior', - actorId, - behaviorType: newBehaviorType || 'none' - } as Payloads.SetBehavior); - } - } - - public lookupAsset(id: Guid): Asset { - if (id === ZeroGuid) { return null; } - - for (const c of this.assetContainers) { - if (c.assetsById.has(id)) { - return c.assetsById.get(id); - } - } - } - - public getStats(): PerformanceStats { - const networkStats = this.protocol.conn.statsReport; - const stats: PerformanceStats = { - actorCount: this.actorSet.size, - actorWithMeshCount: 0, - prefabCount: 0, - materialCount: 0, - textureCount: 0, - texturePixelsTotal: 0, - texturePixelsAverage: 0, - meshCount: 0, - meshVerticesTotal: 0, - meshTrianglesTotal: 0, - soundCount: 0, - soundSecondsTotal: 0, - ...networkStats - }; - - for (const container of this.assetContainers) { - stats.prefabCount += container.prefabs.length; - stats.materialCount += container.materials.length; - stats.textureCount += container.textures.length; - stats.meshCount += container.meshes.length; - stats.soundCount += container.sounds.length; - - for (const tex of container.textures) { - stats.texturePixelsTotal += (tex.texture.resolution.x || 0) * (tex.texture.resolution.y || 0); - } - for (const mesh of container.meshes) { - stats.meshTrianglesTotal += mesh.mesh.triangleCount || 0; - stats.meshVerticesTotal += mesh.mesh.vertexCount || 0; - } - for (const sound of container.sounds) { - stats.soundSecondsTotal += sound.sound.duration || 0; - } - } - stats.texturePixelsAverage = stats.texturePixelsTotal / (stats.textureCount || 1); - - for (const actor of this.actorSet.values()) { - if (actor.appearance.activeAndEnabled && actor.appearance.mesh) { - stats.actorWithMeshCount += 1; - } - } - - return stats; - } -} +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +import { + ActionEvent, + Actor, + ActorLike, + Animation, + AnimationLike, + AnimationWrapMode, + Asset, + AssetContainer, + AssetContainerIterable, + AssetLike, + BehaviorType, + CollisionEvent, + CollisionLayer, + Context, + CreateAnimationOptions, + Guid, + log, + MediaCommand, + MediaInstance, + newGuid, + PerformanceStats, + SetAnimationStateOptions, + SetMediaStateOptions, + TriggerEvent, + User, + UserLike, + ZeroGuid, +} from '..'; +import { + ExportedPromise, + OperatingModel, + Patchable, + Payloads, + Protocols, + resolveJsonValues, + safeAccessPath as safeGet, +} from '../internal'; + +/** + * @hidden + */ +export class ContextInternal { + public actorSet = new Map(); + public userSet = new Map(); + public userGroupMapping: { [id: string]: number } = { default: 1 }; + public assetContainers = new Set(); + public animationSet: Map = new Map(); + public protocol: Protocols.Protocol; + public interval: NodeJS.Timer; + public generation = 0; + public prevGeneration = 0; + public __rpc: any; + + constructor(public context: Context) { + // Handle connection close events. + this.onClose = this.onClose.bind(this); + this.context.conn.on('close', this.onClose); + } + + public Create(options?: { + actor?: Partial; + }): Actor { + return this.createActorFromPayload({ + ...options, + actor: { + ...(options && options.actor), + id: newGuid() + }, + type: 'create-empty' + } as Payloads.CreateEmpty); + } + + public CreateFromLibrary(options: { + resourceId: string; + actor?: Partial; + }): Actor { + return this.createActorFromPayload({ + ...options, + actor: { + ...(options && options.actor), + id: newGuid() + }, + type: 'create-from-library' + } as Payloads.CreateFromLibrary); + } + + public CreateFromPrefab(options: { + prefabId: Guid; + collisionLayer?: CollisionLayer; + actor?: Partial; + }): Actor { + return this.createActorFromPayload({ + ...options, + actor: { + ...(options && options.actor), + id: newGuid() + }, + type: 'create-from-prefab' + } as Payloads.CreateFromPrefab); + } + + private createActorFromPayload( + payload: Payloads.CreateActorCommon + ): Actor { + // Resolve by-reference values now, ensuring they won't change in the + // time between now and when this message is actually sent. + payload.actor = Actor.sanitize(payload.actor); + // Create the actor locally. + this.updateActors(payload.actor); + // Get a reference to the new actor. + const actor = this.context.actor(payload.actor.id); + + this.protocol.sendPayload( payload, { + resolve: (replyPayload: Payloads.ObjectSpawned | Payloads.OperationResult) => { + this.protocol.recvPayload(replyPayload); + let success: boolean; + let message: string; + if (replyPayload.type === 'operation-result') { + success = replyPayload.resultCode !== 'error'; + message = replyPayload.message; + } else { + success = replyPayload.result.resultCode !== 'error'; + message = replyPayload.result.message; + + for (const createdAnimLike of replyPayload.animations) { + if (!this.animationSet.has(createdAnimLike.id)) { + const createdAnim = new Animation(this.context, createdAnimLike.id); + createdAnim.copy(createdAnimLike); + this.animationSet.set(createdAnimLike.id, createdAnim); + } + } + + for (const createdActorLike of replyPayload.actors) { + const createdActor = this.actorSet.get(createdActorLike.id); + if (createdActor) { + createdActor.internal.notifyCreated(success, replyPayload.result.message); + } + } + } + + if (success) { + if (!actor.collider && actor.internal.behavior) { + log.warning('app', 'Behaviors will not function on Unity host apps without adding a' + + ' collider to this actor first. Recommend adding a primitive collider' + + ' to this actor.'); + } + actor.internal.notifyCreated(true); + } else { + actor.internal.notifyCreated(false, message); + } + }, + reject: (reason?: any) => { + actor.internal.notifyCreated(false, reason); + } + }); + + return actor; + } + + public CreateFromGltf(container: AssetContainer, options: { + uri: string; + colliderType?: 'box' | 'mesh'; + actor?: Partial; + }): Actor { + // create actor locally + options.actor = Actor.sanitize({ ...options.actor, id: newGuid() }); + this.updateActors(options.actor); + const actor = this.context.actor(options.actor.id); + + // reserve actor so the pending actor is ready for commands + this.protocol.sendPayload({ + type: 'x-reserve-actor', + actor: options.actor + } as Payloads.XReserveActor); + + // kick off asset loading + container.loadGltf(options.uri, options.colliderType) + .then(assets => { + if (!this.context.actor(actor.id)) { + // actor was destroyed, stop loading + return; + } + + // once assets are done, find first prefab... + const prefab = assets.find(a => !!a.prefab); + if (!prefab) { + actor.internal.notifyCreated(false, `glTF contains no prefabs: ${options.uri}`); + return; + } + + // ...and spawn it + this.createActorFromPayload({ + type: 'create-from-prefab', + prefabId: prefab.id, + actor: options.actor + } as Payloads.CreateFromPrefab); + }) + .catch(reason => actor.internal.notifyCreated(false, reason)); + + return actor; + } + + public createAnimation(actorId: Guid, animationName: string, options: CreateAnimationOptions) { + const actor = this.actorSet.get(actorId); + if (!actor) { + log.error('app', `Failed to create animation on ${animationName}. Actor ${actorId} not found.`); + } + options = { + wrapMode: AnimationWrapMode.Once, + ...options + }; + + // Transform animations must be specified in local space + for (const frame of options.keyframes) { + if (frame.value.transform && !safeGet(frame.value, 'transform', 'local')) { + throw new Error("Transform animations must be specified in local space"); + } + } + + // generate the anim immediately + const createdAnim = new Animation(this.context, newGuid()); + createdAnim.copy({ + name: animationName, + targetActorIds: [actorId], + weight: options.initialState?.enabled === true ? 1 : 0, + speed: options.initialState?.speed, + time: options.initialState?.time + }); + this.animationSet.set(createdAnim.id, createdAnim); + + // Resolve by-reference values now, ensuring they won't change in the + // time between now and when this message is actually sent. + options.keyframes = resolveJsonValues(options.keyframes); + return new Promise((resolve, reject) => { + this.protocol.sendPayload({ + type: 'create-animation', + actorId, + animationName, + animationId: createdAnim.id.toString(), + ...options + } as Payloads.CreateAnimation, + { + resolve: (reply: Payloads.ObjectSpawned) => { + if (reply.result.resultCode !== 'error') { + createdAnim.copy(reply.animations[0]); + resolve(createdAnim); + } else { + reject(reply.result.message); + } + }, + reject + }); + }); + } + + public setAnimationState( + actorId: Guid, + animationName: string, + state: SetAnimationStateOptions + ) { + const actor = this.actorSet.get(actorId); + if (!actor) { + log.error('app', `Failed to set animation state on "${animationName}". Actor "${actorId}" not found.`); + return; + } + const anim = actor.animationsByName.get(animationName); + if (!anim) { + log.error('app', `Failed to set animation state on "${animationName}". ` + + `No animation with this name was found on actor "${actorId}" (${actor.name}).`); + return; + } + if (state.enabled !== undefined) { + anim.isPlaying = state.enabled; + } + if (state.speed !== undefined) { + anim.speed = state.speed; + } + if (state.time !== undefined) { + anim.time = state.time; + } + } + + public setMediaState( + mediaInstance: MediaInstance, + command: MediaCommand, + options?: SetMediaStateOptions, + mediaAssetId?: Guid, + ) { + this.protocol.sendPayload({ + type: 'set-media-state', + id: mediaInstance.id, + actorId: mediaInstance.actor.id, + mediaAssetId, + mediaCommand: command, + options + } as Payloads.SetMediaState); + } + + public animateTo( + actorId: Guid, + value: Partial, + duration: number, + curve: number[], + ) { + const actor = this.actorSet.get(actorId); + if (!actor) { + log.error('app', `Failed animateTo. Actor ${actorId} not found.`); + } else if (!Array.isArray(curve) || curve.length !== 4) { + log.error('app', '`curve` parameter must be an array of four numbers. ' + + 'Try passing one of the predefined curves from `AnimationEaseCurves`'); + } else { + this.protocol.sendPayload({ + type: 'interpolate-actor', + actorId, + animationName: newGuid().toString(), + value, + duration, + curve, + enabled: true + } as Payloads.InterpolateActor); + } + } + + public async startListening() { + try { + // Startup the handshake protocol. + const handshake = this.protocol = + new Protocols.Handshake(this.context.conn, this.context.sessionId, OperatingModel.ServerAuthoritative); + await handshake.run(); + + // Switch to execution protocol. + const execution = this.protocol = new Protocols.Execution(this.context); + + execution.on('protocol.update-actors', this.updateActors.bind(this)); + execution.on('protocol.destroy-actors', this.localDestroyActors.bind(this)); + execution.on('protocol.user-joined', this.userJoined.bind(this)); + execution.on('protocol.user-left', this.userLeft.bind(this)); + execution.on('protocol.update-user', this.updateUser.bind(this)); + execution.on('protocol.perform-action', this.performAction.bind(this)); + execution.on('protocol.receive-rpc', this.receiveRPC.bind(this)); + execution.on('protocol.collision-event-raised', this.collisionEventRaised.bind(this)); + execution.on('protocol.trigger-event-raised', this.triggerEventRaised.bind(this)); + execution.on('protocol.set-animation-state', this.setAnimationStateEventRaised.bind(this)); + execution.on('protocol.update-animations', this.updateAnimations.bind(this)); + + // Startup the execution protocol + execution.startListening(); + } catch (e) { + log.error('app', e); + } + } + + public start() { + if (!this.interval) { + this.interval = setInterval(() => this.update(), 0); + this.context.emitter.emit('started'); + } + } + + public stop() { + try { + if (this.interval) { + this.protocol.stopListening(); + clearInterval(this.interval); + this.interval = undefined; + this.context.emitter.emit('stopped'); + this.context.emitter.removeAllListeners(); + } + } catch { } + } + + public incrementGeneration() { + this.generation++; + } + + private assetsIterable() { + return new AssetContainerIterable([...this.assetContainers]); + } + + public update() { + // Early out if no state changes occurred. + if (this.generation === this.prevGeneration) { + return; + } + + this.prevGeneration = this.generation; + + const syncObjects = [ + ...this.actorSet.values(), + ...this.assetsIterable(), + ...this.userSet.values(), + ...this.animationSet.values() + ] as Array>; + + for (const patchable of syncObjects) { + const patch = patchable.internal.getPatchAndReset(); + if (!patch) { + continue; + } + + if (patchable instanceof Actor) { + this.protocol.sendPayload({ + type: 'actor-update', + actor: patch as ActorLike + } as Payloads.ActorUpdate); + } else if (patchable instanceof Animation) { + this.protocol.sendPayload({ + type: 'animation-update', + animation: patch as Partial + } as Payloads.AnimationUpdate) + } else if (patchable instanceof Asset) { + this.protocol.sendPayload({ + type: 'asset-update', + asset: patch as AssetLike + } as Payloads.AssetUpdate); + } else if (patchable instanceof User) { + this.protocol.sendPayload({ + type: 'user-update', + user: patch as UserLike + } as Payloads.UserUpdate); + } + } + + if (this.nextUpdatePromise) { + this.resolveNextUpdatePromise(); + this.nextUpdatePromise = null; + this.resolveNextUpdatePromise = null; + } + } + + private nextUpdatePromise: Promise; + private resolveNextUpdatePromise: () => void; + /** @hidden */ + public nextUpdate(): Promise { + if (this.nextUpdatePromise) { + return this.nextUpdatePromise; + } + + return this.nextUpdatePromise = new Promise(resolve => { + this.resolveNextUpdatePromise = resolve; + }); + } + + public sendDestroyActors(actorIds: Guid[]) { + if (actorIds.length) { + this.protocol.sendPayload({ + type: 'destroy-actors', + actorIds, + } as Payloads.DestroyActors); + } + } + + public updateActors(sactors: Partial | Array>) { + if (!sactors) { + return; + } + if (!Array.isArray(sactors)) { + sactors = [sactors]; + } + const newActorIds: Guid[] = []; + // Instantiate and store each actor. + sactors.forEach(sactor => { + const isNewActor = !this.actorSet.get(sactor.id); + const actor = isNewActor ? Actor.alloc(this.context, sactor.id) : this.actorSet.get(sactor.id); + this.actorSet.set(sactor.id, actor); + actor.copy(sactor); + if (isNewActor) { + newActorIds.push(actor.id); + } + }); + newActorIds.forEach(actorId => { + const actor = this.actorSet.get(actorId); + this.context.emitter.emit('actor-created', actor); + }); + } + + public updateAnimations(animPatches: Array>) { + if (!animPatches) { return; } + const newAnims: Animation[] = []; + for (const patch of animPatches) { + if (this.animationSet.has(patch.id)) { continue; } + const newAnim = new Animation(this.context, patch.id); + this.animationSet.set(newAnim.id, newAnim); + newAnim.copy(patch); + newAnims.push(newAnim); + } + for (const anim of newAnims) { + this.context.emitter.emit('animation-created', anim); + } + } + + public sendPayload(payload: Payloads.Payload, promise?: ExportedPromise): void { + this.protocol.sendPayload(payload, promise); + } + + public receiveRPC(payload: Payloads.EngineToAppRPC) { + this.context.receiveRPC(payload); + } + + public onClose = () => { + this.stop(); + }; + + public userJoined(suser: Partial) { + if (!this.userSet.has(suser.id)) { + const user = new User(this.context, suser.id); + this.userSet.set(suser.id, user); + user.copy(suser); + this.context.emitter.emit('user-joined', user); + } + } + + public userLeft(userId: Guid) { + const user = this.userSet.get(userId); + if (user) { + this.userSet.delete(userId); + this.context.emitter.emit('user-left', user); + } + } + + public updateUser(suser: Partial) { + let user = this.userSet.get(suser.id); + if (!user) { + user = new User(this.context, suser.id); + user.copy(suser); + this.userSet.set(user.id, user); + this.context.emitter.emit('user-joined', user); + } else { + user.copy(suser); + this.context.emitter.emit('user-updated', user); + } + } + + public performAction(actionEvent: ActionEvent) { + if (actionEvent.user) { + const targetActor = this.actorSet.get(actionEvent.targetId); + if (targetActor) { + targetActor.internal.performAction(actionEvent); + } + } + } + + public collisionEventRaised(collisionEvent: CollisionEvent) { + const actor = this.actorSet.get(collisionEvent.colliderOwnerId); + const otherActor = this.actorSet.get((collisionEvent.collisionData.otherActorId)); + if (actor && otherActor) { + // Update the collision data to contain the actual other actor. + collisionEvent.collisionData = { + ...collisionEvent.collisionData, + otherActor + }; + + actor.internal.collisionEventRaised( + collisionEvent.eventType, + collisionEvent.collisionData); + } + } + + public triggerEventRaised(triggerEvent: TriggerEvent) { + const actor = this.actorSet.get(triggerEvent.colliderOwnerId); + const otherActor = this.actorSet.get(triggerEvent.otherColliderOwnerId); + if (actor && otherActor) { + actor.internal.triggerEventRaised( + triggerEvent.eventType, + otherActor); + } + } + + public setAnimationStateEventRaised(actorId: Guid, animationName: string, state: SetAnimationStateOptions) { + const actor = this.context.actor(actorId); + if (actor) { + actor.internal.setAnimationStateEventRaised(animationName, state); + } + } + + public localDestroyActors(actorIds: Guid[]) { + for (const actorId of actorIds) { + if (this.actorSet.has(actorId)) { + this.localDestroyActor(this.actorSet.get(actorId)); + } + } + } + + public localDestroyActor(actor: Actor) { + // Recursively destroy children first + (actor.children || []).forEach(child => { + this.localDestroyActor(child); + }); + // Remove actor from _actors + this.actorSet.delete(actor.id); + // Raise event + this.context.emitter.emit('actor-destroyed', actor); + } + + public destroyActor(actorId: Guid) { + const actor = this.actorSet.get(actorId); + if (actor) { + // Tell engine to destroy the actor (will destroy all children too) + this.sendDestroyActors([actorId]); + // Clean up the actor locally + this.localDestroyActor(actor); + } + } + + public sendRigidBodyCommand(actorId: Guid, payload: Payloads.Payload) { + this.protocol.sendPayload({ + type: 'rigidbody-commands', + actorId, + commandPayloads: [payload] + } as Payloads.RigidBodyCommands); + } + + public setBehavior(actorId: Guid, newBehaviorType: BehaviorType) { + const actor = this.actorSet.get(actorId); + if (actor) { + this.protocol.sendPayload({ + type: 'set-behavior', + actorId, + behaviorType: newBehaviorType || 'none' + } as Payloads.SetBehavior); + } + } + + public lookupAsset(id: Guid): Asset { + if (id === ZeroGuid) { return null; } + + for (const c of this.assetContainers) { + if (c.assetsById.has(id)) { + return c.assetsById.get(id); + } + } + } + + public getStats(): PerformanceStats { + const networkStats = this.protocol.conn.statsReport; + const stats: PerformanceStats = { + actorCount: this.actorSet.size, + actorWithMeshCount: 0, + prefabCount: 0, + materialCount: 0, + textureCount: 0, + texturePixelsTotal: 0, + texturePixelsAverage: 0, + meshCount: 0, + meshVerticesTotal: 0, + meshTrianglesTotal: 0, + soundCount: 0, + soundSecondsTotal: 0, + ...networkStats + }; + + for (const container of this.assetContainers) { + stats.prefabCount += container.prefabs.length; + stats.materialCount += container.materials.length; + stats.textureCount += container.textures.length; + stats.meshCount += container.meshes.length; + stats.soundCount += container.sounds.length; + + for (const tex of container.textures) { + stats.texturePixelsTotal += (tex.texture.resolution.x || 0) * (tex.texture.resolution.y || 0); + } + for (const mesh of container.meshes) { + stats.meshTrianglesTotal += mesh.mesh.triangleCount || 0; + stats.meshVerticesTotal += mesh.mesh.vertexCount || 0; + } + for (const sound of container.sounds) { + stats.soundSecondsTotal += sound.sound.duration || 0; + } + } + stats.texturePixelsAverage = stats.texturePixelsTotal / (stats.textureCount || 1); + + for (const actor of this.actorSet.values()) { + if (actor.appearance.activeAndEnabled && actor.appearance.mesh) { + stats.actorWithMeshCount += 1; + } + } + + return stats; + } +} diff --git a/packages/sdk/src/core/index.ts b/packages/sdk/src/core/index.ts new file mode 100644 index 000000000..43cde3929 --- /dev/null +++ b/packages/sdk/src/core/index.ts @@ -0,0 +1,12 @@ +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +export * from './context'; +export * from './multipeerAdapter'; +export * from './parameterSet'; +export * from './performanceStats'; +export * from './primitiveTypes'; +export * from './rpc'; +export * from './websocketAdapter'; diff --git a/packages/sdk/src/adapters/multipeer/adapter.ts b/packages/sdk/src/core/multipeerAdapter.ts similarity index 91% rename from packages/sdk/src/adapters/multipeer/adapter.ts rename to packages/sdk/src/core/multipeerAdapter.ts index f97877e20..39ab6b5fe 100644 --- a/packages/sdk/src/adapters/multipeer/adapter.ts +++ b/packages/sdk/src/core/multipeerAdapter.ts @@ -1,171 +1,182 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -import * as http from 'http'; -import QueryString from 'query-string'; -import * as Restify from 'restify'; -import semver from 'semver'; -import UUID from 'uuid/v4'; -import * as WS from 'ws'; -import { Adapter, AdapterOptions, ClientHandshake, ClientStartup } from '..'; -import { Context, ParameterSet, Pipe, WebSocket } from '../../'; -import * as Constants from '../../constants'; -import verifyClient from '../../utils/verifyClient'; -import { log } from './../../log'; -import { Client } from './client'; -import { Session } from './session'; - -const forwarded: (res: http.IncomingMessage, headers: http.IncomingHttpHeaders) => {ip: string; port: number} - = require('forwarded-for'); /* eslint-disable-line @typescript-eslint/no-var-requires */ - -/** - * Multi-peer adapter options - */ -export type MultipeerAdapterOptions = AdapterOptions & { - /** - * @member peerAuthoritative (Optional. Default: true) Whether or not to run in the `peer-authoritative` - * operating model. When true, one peer is picked to synchonize actor changes, animation states, etc. - * When false, no state is synchronized between peers. - */ - peerAuthoritative?: boolean; -}; - -/** - * The `MultipeerAdapter` is appropriate to use when the host environment has no authoritative - * server simulation, where each client owns some part of the simulation, and a connection from each client to the Mixed - * Reality Extension (MRE) app is necessary. The MultipeerAdapter serves as an aggregation point for these client - * connections. This adapter is responsible for app state synchronization to new clients, and for managing distributed - * state ownership (i.e., which client is authoritative over what parts of the simulated state). - * - * Example hosts: - * - AltspaceVR - * - Peer-to-peer multiuser topologies - */ -export class MultipeerAdapter extends Adapter { - - // FUTURE: Make these child processes? - private sessions: { [id: string]: Session } = {}; - - /** @override */ - protected get options(): MultipeerAdapterOptions { return this._options; } - - /** - * Creates a new instance of the Multi-peer Adapter - */ - constructor(options?: MultipeerAdapterOptions) { - super(options); - this._options = { peerAuthoritative: true, ...this._options } as AdapterOptions; - } - - /** - * Start the adapter listening for new incoming connections from engine clients - */ - public listen() { - if (!this.server) { - // If necessary, create a new web server - return new Promise((resolve) => { - const server = this.server = Restify.createServer({ name: "Multi-peer Adapter" }); - this.server.listen(this.port, () => { - this.startListening(); - resolve(server); - }); - }); - } else { - // Already have a server, so just start listening - this.startListening(); - return Promise.resolve(this.server); - } - } - - private async getOrCreateSession(sessionId: string, params: ParameterSet) { - let session = this.sessions[sessionId]; - if (!session) { - // Create an in-memory "connection" (If the app were running remotely, we would connect - // to it via WebSocket here instead) - const pipe = new Pipe(); - pipe.local.statsTracker.on('incoming', bytes => pipe.remote.statsTracker.recordIncoming(bytes)); - pipe.local.statsTracker.on('outgoing', bytes => pipe.remote.statsTracker.recordOutgoing(bytes)); - pipe.local.on('linkQuality', quality => pipe.remote.linkConnectionQuality(quality)); - - // Create a new context for the connection, passing it the remote side of the pipe. - const context = new Context({ - sessionId, - connection: pipe.remote - }); - // Start the context listening to network traffic. - context.internal.startListening().catch(() => pipe.remote.close()); - // Instantiate a new session. - session = this.sessions[sessionId] = new Session( - pipe.local, sessionId, this.options.peerAuthoritative); - // Handle session close. - session.on('close', () => delete this.sessions[sessionId]); - // Connect the session to the context. - await session.connect(); // Allow exceptions to propagate. - // Pass the new context to the app. - this.emitter.emit('connection', context, params); - // Start context's update loop. - context.internal.start(); - } - return session; - } - - private startListening() { - // Create a server for upgrading HTTP connections to WebSockets - const wss = new WS.Server({ server: this.server, verifyClient }); - - // Handle WebSocket connection upgrades - wss.on('connection', async (ws: WS, request: http.IncomingMessage) => { - try { - log.info('network', "New Multi-peer connection"); - - // Read the sessionId header. - let sessionId = request.headers[Constants.HTTPHeaders.SessionID] as string || UUID(); - sessionId = decodeURIComponent(sessionId); - - // Read the client's version number - const version = semver.coerce(request.headers[Constants.HTTPHeaders.CurrentClientVersion] as string); - - // Parse URL parameters. - const params = QueryString.parseUrl(request.url).query; - - // Get the client's IP address rather than the last proxy connecting to you. - const address = forwarded(request, request.headers); - - // Create a WebSocket for this connection. - const conn = new WebSocket(ws, address.ip); - - // Instantiate a client for this connection. - const client = new Client(conn, version); - - // Join the client to the session. - await this.joinClientToSession(client, sessionId, params); - } catch (e) { - log.error('network', e); - ws.close(); - } - }); - } - - private async joinClientToSession(client: Client, sessionId: string, params: QueryString.OutputParams) { - try { - // Handshake with the client. - const handshake = new ClientHandshake(client, sessionId); - await handshake.run(); - - // Measure the connection quality and wait for sync-request message. - const startup = new ClientStartup(client, handshake.syncRequest); - await startup.run(); - - // Get the session for the sessionId. - const session = await this.getOrCreateSession(sessionId, params); - - // Join the client to the session. - await session.join(client); - } catch (e) { - log.error('network', e); - client.conn.close(); - } - } -} +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import * as http from 'http'; +import QueryString from 'query-string'; +import * as Restify from 'restify'; +import semver from 'semver'; +import UUID from 'uuid/v4'; +import * as WS from 'ws'; + +import { + Context, + log, + ParameterSet +} from '..'; +import { + Adapter, + AdapterOptions, + Client, + ClientHandshake, + ClientStartup, + Constants, + Pipe, + Session, + verifyClient, + WebSocket +} from '../internal'; + +const forwarded: (res: http.IncomingMessage, headers: http.IncomingHttpHeaders) => {ip: string; port: number} + = require('forwarded-for'); /* eslint-disable-line @typescript-eslint/no-var-requires */ + +/** + * Multi-peer adapter options + */ +export type MultipeerAdapterOptions = AdapterOptions & { + /** + * @member peerAuthoritative (Optional. Default: true) Whether or not to run in the `peer-authoritative` + * operating model. When true, one peer is picked to synchonize actor changes, animation states, etc. + * When false, no state is synchronized between peers. + */ + peerAuthoritative?: boolean; +}; + +/** + * The `MultipeerAdapter` is appropriate to use when the host environment has no authoritative + * server simulation, where each client owns some part of the simulation, and a connection from each client to the Mixed + * Reality Extension (MRE) app is necessary. The MultipeerAdapter serves as an aggregation point for these client + * connections. This adapter is responsible for app state synchronization to new clients, and for managing distributed + * state ownership (i.e., which client is authoritative over what parts of the simulated state). + * + * Example hosts: + * - AltspaceVR + * - Peer-to-peer multiuser topologies + */ +export class MultipeerAdapter extends Adapter { + + // FUTURE: Make these child processes? + private sessions: { [id: string]: Session } = {}; + + /** @override */ + protected get options(): MultipeerAdapterOptions { return this._options; } + + /** + * Creates a new instance of the Multi-peer Adapter + */ + constructor(options?: MultipeerAdapterOptions) { + super(options); + this._options = { peerAuthoritative: true, ...this._options } as AdapterOptions; + } + + /** + * Start the adapter listening for new incoming connections from engine clients + */ + public listen() { + if (!this.server) { + // If necessary, create a new web server + return new Promise((resolve) => { + const server = this.server = Restify.createServer({ name: "Multi-peer Adapter" }); + this.server.listen(this.port, () => { + this.startListening(); + resolve(server); + }); + }); + } else { + // Already have a server, so just start listening + this.startListening(); + return Promise.resolve(this.server); + } + } + + private async getOrCreateSession(sessionId: string, params: ParameterSet) { + let session = this.sessions[sessionId]; + if (!session) { + // Create an in-memory "connection" (If the app were running remotely, we would connect + // to it via WebSocket here instead) + const pipe = new Pipe(); + pipe.local.statsTracker.on('incoming', bytes => pipe.remote.statsTracker.recordIncoming(bytes)); + pipe.local.statsTracker.on('outgoing', bytes => pipe.remote.statsTracker.recordOutgoing(bytes)); + pipe.local.on('linkQuality', quality => pipe.remote.linkConnectionQuality(quality)); + + // Create a new context for the connection, passing it the remote side of the pipe. + const context = new Context({ + sessionId, + connection: pipe.remote + }); + // Start the context listening to network traffic. + context.internal.startListening().catch(() => pipe.remote.close()); + // Instantiate a new session. + session = this.sessions[sessionId] = new Session( + pipe.local, sessionId, this.options.peerAuthoritative); + // Handle session close. + session.on('close', () => delete this.sessions[sessionId]); + // Connect the session to the context. + await session.connect(); // Allow exceptions to propagate. + // Pass the new context to the app. + this.emitter.emit('connection', context, params); + // Start context's update loop. + context.internal.start(); + } + return session; + } + + private startListening() { + // Create a server for upgrading HTTP connections to WebSockets + const wss = new WS.Server({ server: this.server, verifyClient }); + + // Handle WebSocket connection upgrades + wss.on('connection', async (ws: WS, request: http.IncomingMessage) => { + try { + log.info('network', "New Multi-peer connection"); + + // Read the sessionId header. + let sessionId = request.headers[Constants.HTTPHeaders.SessionID] as string || UUID(); + sessionId = decodeURIComponent(sessionId); + + // Read the client's version number + const version = semver.coerce(request.headers[Constants.HTTPHeaders.CurrentClientVersion] as string); + + // Parse URL parameters. + const params = QueryString.parseUrl(request.url).query; + + // Get the client's IP address rather than the last proxy connecting to you. + const address = forwarded(request, request.headers); + + // Create a WebSocket for this connection. + const conn = new WebSocket(ws, address.ip); + + // Instantiate a client for this connection. + const client = new Client(conn, version); + + // Join the client to the session. + await this.joinClientToSession(client, sessionId, params); + } catch (e) { + log.error('network', e); + ws.close(); + } + }); + } + + private async joinClientToSession(client: Client, sessionId: string, params: QueryString.OutputParams) { + try { + // Handshake with the client. + const handshake = new ClientHandshake(client, sessionId); + await handshake.run(); + + // Measure the connection quality and wait for sync-request message. + const startup = new ClientStartup(client, handshake.syncRequest); + await startup.run(); + + // Get the session for the sessionId. + const session = await this.getOrCreateSession(sessionId, params); + + // Join the client to the session. + await session.join(client); + } catch (e) { + log.error('network', e); + client.conn.close(); + } + } +} diff --git a/packages/sdk/src/types/parameterSet.ts b/packages/sdk/src/core/parameterSet.ts similarity index 95% rename from packages/sdk/src/types/parameterSet.ts rename to packages/sdk/src/core/parameterSet.ts index 9dce4244e..1606f2682 100644 --- a/packages/sdk/src/types/parameterSet.ts +++ b/packages/sdk/src/core/parameterSet.ts @@ -1,11 +1,11 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -/** - * Container for a set of parameters. - */ -export interface ParameterSet { - [key: string]: string | string[] | undefined; -} +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +/** + * Container for a set of parameters. + */ +export interface ParameterSet { + [key: string]: string | string[] | undefined; +} diff --git a/packages/sdk/src/types/performanceStats.ts b/packages/sdk/src/core/performanceStats.ts similarity index 100% rename from packages/sdk/src/types/performanceStats.ts rename to packages/sdk/src/core/performanceStats.ts diff --git a/packages/sdk/src/types/primitiveTypes.ts b/packages/sdk/src/core/primitiveTypes.ts similarity index 95% rename from packages/sdk/src/types/primitiveTypes.ts rename to packages/sdk/src/core/primitiveTypes.ts index fbab03491..7dbcd03b9 100644 --- a/packages/sdk/src/types/primitiveTypes.ts +++ b/packages/sdk/src/core/primitiveTypes.ts @@ -1,93 +1,93 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -import { Vector3Like } from '..'; - -/** - * Describes the general shape of a primitive. Specifics are described in a [[PrimitiveDefinition]] object. - */ -export enum PrimitiveShape { - Sphere = 'sphere', - Box = 'box', - Capsule = 'capsule', - Cylinder = 'cylinder', - Plane = 'plane' -} - -export type SpherePrimitiveDefinition = { - shape: PrimitiveShape.Sphere; - /** - * The bounding box size of the primitive. - */ - dimensions?: Partial; - /** - * The number of horizontal or radial segments of spheres, cylinders, capsules, and planes. - */ - uSegments?: number; - /** - * The number of vertical or axial segments of spheres, capsules, and planes. - */ - vSegments?: number; -}; - -export type BoxPrimitiveDefinition = { - shape: PrimitiveShape.Box; - /** - * The bounding box size of the primitive. - */ - dimensions?: Partial; -}; - -export type CapsulePrimitiveDefinition = { - shape: PrimitiveShape.Capsule; - /** - * The bounding box size of the primitive. - */ - dimensions?: Partial; - /** - * The number of horizontal or radial segments of spheres, cylinders, capsules, and planes. - */ - uSegments?: number; - /** - * The number of vertical or axial segments of spheres, capsules, and planes. - */ - vSegments?: number; -}; - -export type CylinderPrimitiveDefinition = { - shape: PrimitiveShape.Cylinder; - /** - * The bounding box size of the primitive. - */ - dimensions?: Partial; - /** - * The number of horizontal or radial segments of spheres, cylinders, capsules, and planes. - */ - uSegments?: number; -}; - -export type PlanePrimitiveDefinition = { - shape: PrimitiveShape.Plane; - /** - * The bounding box size of the primitive. - */ - dimensions?: Partial; - /** - * The number of horizontal or radial segments of spheres, cylinders, capsules, and planes. - */ - uSegments?: number; - /** - * The number of vertical or axial segments of spheres, capsules, and planes. - */ - vSegments?: number; -}; - -export type PrimitiveDefinition - = SpherePrimitiveDefinition - | BoxPrimitiveDefinition - | CapsulePrimitiveDefinition - | CylinderPrimitiveDefinition - | PlanePrimitiveDefinition - ; +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { Vector3Like } from '..'; + +/** + * Describes the general shape of a primitive. Specifics are described in a [[PrimitiveDefinition]] object. + */ +export enum PrimitiveShape { + Sphere = 'sphere', + Box = 'box', + Capsule = 'capsule', + Cylinder = 'cylinder', + Plane = 'plane' +} + +export type SpherePrimitiveDefinition = { + shape: PrimitiveShape.Sphere; + /** + * The bounding box size of the primitive. + */ + dimensions?: Partial; + /** + * The number of horizontal or radial segments of spheres, cylinders, capsules, and planes. + */ + uSegments?: number; + /** + * The number of vertical or axial segments of spheres, capsules, and planes. + */ + vSegments?: number; +}; + +export type BoxPrimitiveDefinition = { + shape: PrimitiveShape.Box; + /** + * The bounding box size of the primitive. + */ + dimensions?: Partial; +}; + +export type CapsulePrimitiveDefinition = { + shape: PrimitiveShape.Capsule; + /** + * The bounding box size of the primitive. + */ + dimensions?: Partial; + /** + * The number of horizontal or radial segments of spheres, cylinders, capsules, and planes. + */ + uSegments?: number; + /** + * The number of vertical or axial segments of spheres, capsules, and planes. + */ + vSegments?: number; +}; + +export type CylinderPrimitiveDefinition = { + shape: PrimitiveShape.Cylinder; + /** + * The bounding box size of the primitive. + */ + dimensions?: Partial; + /** + * The number of horizontal or radial segments of spheres, cylinders, capsules, and planes. + */ + uSegments?: number; +}; + +export type PlanePrimitiveDefinition = { + shape: PrimitiveShape.Plane; + /** + * The bounding box size of the primitive. + */ + dimensions?: Partial; + /** + * The number of horizontal or radial segments of spheres, cylinders, capsules, and planes. + */ + uSegments?: number; + /** + * The number of vertical or axial segments of spheres, capsules, and planes. + */ + vSegments?: number; +}; + +export type PrimitiveDefinition + = SpherePrimitiveDefinition + | BoxPrimitiveDefinition + | CapsulePrimitiveDefinition + | CylinderPrimitiveDefinition + | PlanePrimitiveDefinition + ; diff --git a/packages/sdk/src/rpc/rpc.ts b/packages/sdk/src/core/rpc.ts similarity index 89% rename from packages/sdk/src/rpc/rpc.ts rename to packages/sdk/src/core/rpc.ts index 450f58568..2f83a0308 100644 --- a/packages/sdk/src/rpc/rpc.ts +++ b/packages/sdk/src/core/rpc.ts @@ -1,93 +1,93 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ -/* eslint-disable max-classes-per-file */ - -import { Context, Guid } from '..'; -import { AppToEngineRPC, EngineToAppRPC } from '../types/network/payloads'; - -/** - * @hidden - * Type defining an rpc handler function callback. - */ -export type RPCHandler = (options: { userId: Guid }, ...args: any[]) => void; - -/** - * RPC interface. Able to send and receive RPC calls. - */ -export class RPC { - private handlers = new Map(); - - public get context() { return this._context; } - - constructor(protected _context: Context) { - } - - public on(procName: string, handler: RPCHandler) { - if (handler) { - this.handlers.set(procName, handler); - } else { - this.handlers.delete(procName); - } - } - - public removeAllHandlers() { - this.handlers.clear(); - } - - public send( - options: { - procName: string; - channelName?: string; - userId?: Guid; - }, - ...args: any[]) { - this.context.internal.sendPayload({ - type: 'app2engine-rpc', - procName: options.procName, - channelName: options.channelName, - userId: options.userId, - args - } as AppToEngineRPC); - } - - public receive(procName: string, userId: Guid, ...args: any[]) { - const handler = this.handlers.get(procName); - if (handler) { - handler({ userId }, ...args); - } - } -} - -/** - * RPC channel interface. Able to route channel messages to handlers. - */ -export class RPCChannels { - private channelHandlers = new Map(); - private globalHandler: RPC; - - public setChannelHandler(channelName: string, handler: RPC) { - if (channelName) { - if (handler) { - this.channelHandlers.set(channelName, handler); - } else { - this.channelHandlers.delete(channelName); - } - } else { - this.globalHandler = handler; - } - } - - public receive(payload: EngineToAppRPC) { - let handler: RPC; - if (payload.channelName) { - handler = this.channelHandlers.get(payload.channelName); - } else { - handler = this.globalHandler; - } - if (handler) { - handler.receive(payload.procName, payload.userId, ...payload.args); - } - } -} +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +/* eslint-disable max-classes-per-file */ + +import { Context, Guid } from '..'; +import { Payloads } from '../internal'; + +/** + * @hidden + * Type defining an rpc handler function callback. + */ +export type RPCHandler = (options: { userId: Guid }, ...args: any[]) => void; + +/** + * RPC interface. Able to send and receive RPC calls. + */ +export class RPC { + private handlers = new Map(); + + public get context() { return this._context; } + + constructor(protected _context: Context) { + } + + public on(procName: string, handler: RPCHandler) { + if (handler) { + this.handlers.set(procName, handler); + } else { + this.handlers.delete(procName); + } + } + + public removeAllHandlers() { + this.handlers.clear(); + } + + public send( + options: { + procName: string; + channelName?: string; + userId?: Guid; + }, + ...args: any[]) { + this.context.internal.sendPayload({ + type: 'app2engine-rpc', + procName: options.procName, + channelName: options.channelName, + userId: options.userId, + args + } as Payloads.AppToEngineRPC); + } + + public receive(procName: string, userId: Guid, ...args: any[]) { + const handler = this.handlers.get(procName); + if (handler) { + handler({ userId }, ...args); + } + } +} + +/** + * RPC channel interface. Able to route channel messages to handlers. + */ +export class RPCChannels { + private channelHandlers = new Map(); + private globalHandler: RPC; + + public setChannelHandler(channelName: string, handler: RPC) { + if (channelName) { + if (handler) { + this.channelHandlers.set(channelName, handler); + } else { + this.channelHandlers.delete(channelName); + } + } else { + this.globalHandler = handler; + } + } + + public receive(payload: Payloads.EngineToAppRPC) { + let handler: RPC; + if (payload.channelName) { + handler = this.channelHandlers.get(payload.channelName); + } else { + handler = this.globalHandler; + } + if (handler) { + handler.receive(payload.procName, payload.userId, ...payload.args); + } + } +} diff --git a/packages/sdk/src/adapters/websocket/adapter.ts b/packages/sdk/src/core/websocketAdapter.ts similarity index 89% rename from packages/sdk/src/adapters/websocket/adapter.ts rename to packages/sdk/src/core/websocketAdapter.ts index edd3133d4..79dde7ebf 100644 --- a/packages/sdk/src/adapters/websocket/adapter.ts +++ b/packages/sdk/src/core/websocketAdapter.ts @@ -1,99 +1,97 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -import * as http from 'http'; -import QueryString from 'query-string'; -import * as Restify from 'restify'; -import UUID from 'uuid/v4'; -import * as WS from 'ws'; -import { Adapter, AdapterOptions } from '..'; -import { Context, WebSocket } from '../..'; -import * as Constants from '../../constants'; -import verifyClient from '../../utils/verifyClient'; -import { log } from './../../log'; - -const forwarded: (res: http.IncomingMessage, headers: http.IncomingHttpHeaders) => {ip: string; port: number} - = require('forwarded-for'); /* eslint-disable-line @typescript-eslint/no-var-requires */ - -/** - * WebSocket Adapter options. - */ -export type WebSocketAdapterOptions = AdapterOptions; - -/** - * The `WebSocketAdapter` is appropriate to use when the host environment has an authoritative simluation, and that - * authoritative simulation is the only connection made to the Mixed Reality Extension (MRE) app. - * - * Example hosts: - * - Single player environments - * - Server-based multiuser topologies - */ -export class WebSocketAdapter extends Adapter { - /** - * Creates a new instance of the WebSocket Adapter. - */ - constructor(options?: WebSocketAdapterOptions) { - super(options); - } - - /** - * Start the adapter listening for new connections. - * @param onNewConnection Handler for new connections. - */ - public listen() { - if (!this.server) { - // If necessary, create a new web server. - return new Promise((resolve) => { - const server = this.server = Restify.createServer({ name: "WebSocket Adapter" }); - this.server.listen(this.port, () => { - this.startListening(); - resolve(server); - }); - }); - } else { - // Already have a server, so just start listening. - this.startListening(); - return Promise.resolve(this.server); - } - } - - private startListening() { - // Create a server for upgrading HTTP connections to WebSockets. - const wss = new WS.Server({ server: this.server, verifyClient }); - - // Handle connection upgrades - wss.on('connection', (ws: WS, request: http.IncomingMessage) => { - log.info('network', "New WebSocket connection"); - - // Read the sessionId header. - let sessionId = request.headers[Constants.HTTPHeaders.SessionID] as string || UUID(); - sessionId = decodeURIComponent(sessionId); - - // Parse URL parameters. - const params = QueryString.parseUrl(request.url).query; - - // Get the client's IP address rather than the last proxy connecting to you. - const address = forwarded(request, request.headers); - - // Create a WebSocket for the connection. - const connection = new WebSocket(ws, address.ip); - - // Create a new context for the connection. - const context = new Context({ - sessionId, - connection - }); - - // Start the context listening to network traffic. - context.internal.startListening().catch(() => connection.close()); - - // Pass the new context to the app - this.emitter.emit('connection', context, params); - - // Start context's update loop. - context.internal.start(); - }); - } -} +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import * as http from 'http'; +import QueryString from 'query-string'; +import * as Restify from 'restify'; +import UUID from 'uuid/v4'; +import * as WS from 'ws'; + +import { Context, log } from '..'; +import { Adapter, AdapterOptions, Constants, verifyClient, WebSocket } from '../internal'; + +const forwarded: (res: http.IncomingMessage, headers: http.IncomingHttpHeaders) => {ip: string; port: number} + = require('forwarded-for'); /* eslint-disable-line @typescript-eslint/no-var-requires */ + +/** + * WebSocket Adapter options. + */ +export type WebSocketAdapterOptions = AdapterOptions; + +/** + * The `WebSocketAdapter` is appropriate to use when the host environment has an authoritative simluation, and that + * authoritative simulation is the only connection made to the Mixed Reality Extension (MRE) app. + * + * Example hosts: + * - Single player environments + * - Server-based multiuser topologies + */ +export class WebSocketAdapter extends Adapter { + /** + * Creates a new instance of the WebSocket Adapter. + */ + constructor(options?: WebSocketAdapterOptions) { + super(options); + } + + /** + * Start the adapter listening for new connections. + * @param onNewConnection Handler for new connections. + */ + public listen() { + if (!this.server) { + // If necessary, create a new web server. + return new Promise((resolve) => { + const server = this.server = Restify.createServer({ name: "WebSocket Adapter" }); + this.server.listen(this.port, () => { + this.startListening(); + resolve(server); + }); + }); + } else { + // Already have a server, so just start listening. + this.startListening(); + return Promise.resolve(this.server); + } + } + + private startListening() { + // Create a server for upgrading HTTP connections to WebSockets. + const wss = new WS.Server({ server: this.server, verifyClient }); + + // Handle connection upgrades + wss.on('connection', (ws: WS, request: http.IncomingMessage) => { + log.info('network', "New WebSocket connection"); + + // Read the sessionId header. + let sessionId = request.headers[Constants.HTTPHeaders.SessionID] as string || UUID(); + sessionId = decodeURIComponent(sessionId); + + // Parse URL parameters. + const params = QueryString.parseUrl(request.url).query; + + // Get the client's IP address rather than the last proxy connecting to you. + const address = forwarded(request, request.headers); + + // Create a WebSocket for the connection. + const connection = new WebSocket(ws, address.ip); + + // Create a new context for the connection. + const context = new Context({ + sessionId, + connection + }); + + // Start the context listening to network traffic. + context.internal.startListening().catch(() => connection.close()); + + // Pass the new context to the app + this.emitter.emit('connection', context, params); + + // Start context's update loop. + context.internal.start(); + }); + } +} diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index fe4bd3424..ee509f798 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -3,20 +3,10 @@ * Licensed under the MIT License. */ -export * from './adapters'; +export * from './actor'; export * from './animation'; +export * from './asset'; +export * from './core'; export * from './math'; -export * from './connection'; -export * from './media'; -export * from './webHost'; -export * from './log'; -export * from './rpc'; -export * from './types/runtime'; -export * from './types/network'; -export * from './types/rigidBodyConstraints'; -export * from './types/performanceStats'; -export * from './types/primitiveTypes'; -export * from './types/lookatMode'; -export * from './types/parameterSet'; -export * from './types/guid'; -export * from './types/readonlyMap'; +export * from './user'; +export * from './util'; diff --git a/packages/sdk/src/adapters/adapter.ts b/packages/sdk/src/internal/adapters/adapter.ts similarity index 94% rename from packages/sdk/src/adapters/adapter.ts rename to packages/sdk/src/internal/adapters/adapter.ts index e04c085f1..94c63bd82 100644 --- a/packages/sdk/src/adapters/adapter.ts +++ b/packages/sdk/src/internal/adapters/adapter.ts @@ -1,60 +1,61 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -import events from 'events'; -import * as Restify from 'restify'; -import { Context, ParameterSet } from '..'; - -/** - * Adapter options - */ -export type AdapterOptions = { - /** - * @member {http.Server} server Provide an existing web server to use. Will create one otherwise - */ - server?: Restify.Server; - /** - * @member {string | number} port Optional. When options.server is not supplied and an internal web server is to be - * created, this is the port number it should listen on. If this value is not given, it will attempt to read the - * PORT environment variable, then default to 3901 - */ - port?: string | number; -}; - -/** - * Base Adapter class. Adapters are where connections from hosts are accepted and mapped to Contexts. The host - * connection requests a Context from a sessionId. If no matching Context is found, a new one is created and - * the 'connection' event is raised. - */ -export abstract class Adapter { - protected emitter = new events.EventEmitter(); - - protected get options() { return this._options; } - - public get server() { return this._options.server; } - public set server(value: Restify.Server) { this._options.server = value; } - public get port() { return this._options.port; } - - constructor(protected _options: AdapterOptions) { - this._options = { ..._options }; - this._options.port = - this._options.port || - process.env.port || - process.env.PORT || - 3901; - } - - public abstract listen(): Promise; - - /** - * The onConnection event is raised when a new Context is created for an application session. This happens when the - * first client connects to your application. - * @event - */ - public onConnection(handler: (context: Context, params: ParameterSet) => void): this { - this.emitter.addListener('connection', handler); - return this; - } -} +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import events from 'events'; +import * as Restify from 'restify'; + +import { Context, ParameterSet } from '../..'; + +/** + * Adapter options + */ +export type AdapterOptions = { + /** + * @member {http.Server} server Provide an existing web server to use. Will create one otherwise + */ + server?: Restify.Server; + /** + * @member {string | number} port Optional. When options.server is not supplied and an internal web server is to be + * created, this is the port number it should listen on. If this value is not given, it will attempt to read the + * PORT environment variable, then default to 3901 + */ + port?: string | number; +}; + +/** + * Base Adapter class. Adapters are where connections from hosts are accepted and mapped to Contexts. The host + * connection requests a Context from a sessionId. If no matching Context is found, a new one is created and + * the 'connection' event is raised. + */ +export abstract class Adapter { + protected emitter = new events.EventEmitter(); + + protected get options() { return this._options; } + + public get server() { return this._options.server; } + public set server(value: Restify.Server) { this._options.server = value; } + public get port() { return this._options.port; } + + constructor(protected _options: AdapterOptions) { + this._options = { ..._options }; + this._options.port = + this._options.port || + process.env.port || + process.env.PORT || + 3901; + } + + public abstract listen(): Promise; + + /** + * The onConnection event is raised when a new Context is created for an application session. This happens when the + * first client connects to your application. + * @event + */ + public onConnection(handler: (context: Context, params: ParameterSet) => void): this { + this.emitter.addListener('connection', handler); + return this; + } +} diff --git a/packages/sdk/src/adapters/websocket/index.ts b/packages/sdk/src/internal/adapters/index.ts similarity index 81% rename from packages/sdk/src/adapters/websocket/index.ts rename to packages/sdk/src/internal/adapters/index.ts index 8dfc9a26b..6471208c1 100644 --- a/packages/sdk/src/adapters/websocket/index.ts +++ b/packages/sdk/src/internal/adapters/index.ts @@ -1,6 +1,7 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -export * from './adapter'; +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +export * from './adapter'; +export * from './multipeer'; diff --git a/packages/sdk/src/adapters/multipeer/README.md b/packages/sdk/src/internal/adapters/multipeer/README.md similarity index 100% rename from packages/sdk/src/adapters/multipeer/README.md rename to packages/sdk/src/internal/adapters/multipeer/README.md diff --git a/packages/sdk/src/adapters/multipeer/client.ts b/packages/sdk/src/internal/adapters/multipeer/client.ts similarity index 88% rename from packages/sdk/src/adapters/multipeer/client.ts rename to packages/sdk/src/internal/adapters/multipeer/client.ts index f7f5acad4..9f3332d0c 100644 --- a/packages/sdk/src/adapters/multipeer/client.ts +++ b/packages/sdk/src/internal/adapters/multipeer/client.ts @@ -1,149 +1,161 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -import { EventEmitter } from 'events'; -import semver from 'semver'; -import { ClientExecution, ClientSync, MissingRule, Rules, Session } from '.'; -import { Connection, Guid, Message, newGuid } from '../..'; -import { log } from '../../log'; -import * as Protocols from '../../protocols'; -import * as Payloads from '../../types/network/payloads'; -import { ExportedPromise } from '../../utils/exportedPromise'; -import filterEmpty from '../../utils/filterEmpty'; - -/** - * @hidden - */ -export type QueuedMessage = { - message: Message; - promise?: ExportedPromise; - timeoutSeconds?: number; -}; - -/** - * @hidden - * Class representing a connection to an engine client - */ -export class Client extends EventEmitter { - private static orderSequence = 0; - - private _id: Guid; - private _session: Session; - private _protocol: Protocols.Protocol; - private _order: number; - private _queuedMessages: QueuedMessage[] = []; - private _userExclusiveMessages: QueuedMessage[] = []; - private _authoritative = false; - private _leave: () => void; - - public get id() { return this._id; } - public get version() { return this._version; } - public get order() { return this._order; } - public get protocol() { return this._protocol; } - public get session() { return this._session; } - public get conn() { return this._conn; } - public get authoritative() { return this._authoritative; } - public get queuedMessages() { return this._queuedMessages; } - public get userExclusiveMessages() { return this._userExclusiveMessages; } - - public userId: Guid; - - /** - * Creates a new Client instance - */ - constructor(private _conn: Connection, private _version: semver.SemVer) { - super(); - this._id = newGuid(); - this._order = Client.orderSequence++; - this._leave = this.leave.bind(this); - this._conn.on('close', this._leave); - this._conn.on('error', this._leave); - } - - public setAuthoritative(value: boolean) { - this._authoritative = value; - this.protocol.sendPayload({ - type: 'set-authoritative', - authoritative: value - } as Payloads.SetAuthoritative); - } - - /** - * Syncs state with the client - */ - public async join(session: Session) { - try { - this._session = session; - // Sync state to the client - const sync = this._protocol = new ClientSync(this); - await sync.run(); - // Join the session as a user - const execution = this._protocol = new ClientExecution(this); - execution.on('recv', (message) => this.emit('recv', this, message)); - execution.startListening(); - } catch (e) { - log.error('network', e); - this.leave(); - } - } - - public leave() { - try { - if (this._protocol) { - this._protocol.stopListening(); - this._protocol = undefined; - } - this._conn.off('close', this._leave); - this._conn.off('error', this._leave); - this._conn.close(); - this._session = undefined; - this.emit('close'); - } catch { } - } - - public isJoined() { - return this.protocol && this.protocol.constructor.name === "ClientExecution"; - } - - public send(message: Message, promise?: ExportedPromise) { - if (this.protocol) { - this.protocol.sendMessage(message, promise); - } else { - log.error('network', `[ERROR] No protocol for message send: ${message.payload.type}`); - } - } - - public sendPayload(payload: Partial, promise?: ExportedPromise) { - if (this.protocol) { - this.protocol.sendPayload(payload, promise); - } else { - log.error('network', `[ERROR] No protocol for payload send: ${payload.type}`); - } - } - - public queueMessage(message: Message, promise?: ExportedPromise, timeoutSeconds?: number) { - const rule = Rules[message.payload.type] || MissingRule; - const beforeQueueMessageForClient = rule.client.beforeQueueMessageForClient || (() => message); - message = beforeQueueMessageForClient(this.session, this, message, promise); - if (message) { - log.verbose('network', - `Client ${this.id.substr(0, 8)} queue id:${message.id.substr(0, 8)}, type:${message.payload.type}`); - log.verbose('network-content', JSON.stringify(message, (key, value) => filterEmpty(value))); - this.queuedMessages.push({ message, promise, timeoutSeconds }); - } - } - - public filterQueuedMessages(callbackfn: (value: QueuedMessage) => any) { - const filteredMessages: QueuedMessage[] = []; - this._queuedMessages = this._queuedMessages.filter((value) => { - const allow = callbackfn(value); - if (allow) { - filteredMessages.push(value); - } - return !allow; - }); - return filteredMessages; - } -} +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { EventEmitter } from 'events'; +import semver from 'semver'; + +import { + Guid, + log, + newGuid +} from '../../..'; +import { + ClientExecution, + ClientSync, + Connection, + ExportedPromise, + filterEmpty, + Message, + MissingRule, + Payloads, + Protocols, + Rules, + Session +} from '../../../internal'; + +/** + * @hidden + */ +export type QueuedMessage = { + message: Message; + promise?: ExportedPromise; + timeoutSeconds?: number; +}; + +/** + * @hidden + * Class representing a connection to an engine client + */ +export class Client extends EventEmitter { + private static orderSequence = 0; + + private _id: Guid; + private _session: Session; + private _protocol: Protocols.Protocol; + private _order: number; + private _queuedMessages: QueuedMessage[] = []; + private _userExclusiveMessages: QueuedMessage[] = []; + private _authoritative = false; + private _leave: () => void; + + public get id() { return this._id; } + public get version() { return this._version; } + public get order() { return this._order; } + public get protocol() { return this._protocol; } + public get session() { return this._session; } + public get conn() { return this._conn; } + public get authoritative() { return this._authoritative; } + public get queuedMessages() { return this._queuedMessages; } + public get userExclusiveMessages() { return this._userExclusiveMessages; } + + public userId: Guid; + + /** + * Creates a new Client instance + */ + constructor(private _conn: Connection, private _version: semver.SemVer) { + super(); + this._id = newGuid(); + this._order = Client.orderSequence++; + this._leave = this.leave.bind(this); + this._conn.on('close', this._leave); + this._conn.on('error', this._leave); + } + + public setAuthoritative(value: boolean) { + this._authoritative = value; + this.protocol.sendPayload({ + type: 'set-authoritative', + authoritative: value + } as Payloads.SetAuthoritative); + } + + /** + * Syncs state with the client + */ + public async join(session: Session) { + try { + this._session = session; + // Sync state to the client + const sync = this._protocol = new ClientSync(this); + await sync.run(); + // Join the session as a user + const execution = this._protocol = new ClientExecution(this); + execution.on('recv', (message) => this.emit('recv', this, message)); + execution.startListening(); + } catch (e) { + log.error('network', e); + this.leave(); + } + } + + public leave() { + try { + if (this._protocol) { + this._protocol.stopListening(); + this._protocol = undefined; + } + this._conn.off('close', this._leave); + this._conn.off('error', this._leave); + this._conn.close(); + this._session = undefined; + this.emit('close'); + } catch { } + } + + public isJoined() { + return this.protocol && this.protocol.constructor.name === "ClientExecution"; + } + + public send(message: Message, promise?: ExportedPromise) { + if (this.protocol) { + this.protocol.sendMessage(message, promise); + } else { + log.error('network', `[ERROR] No protocol for message send: ${message.payload.type}`); + } + } + + public sendPayload(payload: Partial, promise?: ExportedPromise) { + if (this.protocol) { + this.protocol.sendPayload(payload, promise); + } else { + log.error('network', `[ERROR] No protocol for payload send: ${payload.type}`); + } + } + + public queueMessage(message: Message, promise?: ExportedPromise, timeoutSeconds?: number) { + const rule = Rules[message.payload.type] || MissingRule; + const beforeQueueMessageForClient = rule.client.beforeQueueMessageForClient || (() => message); + message = beforeQueueMessageForClient(this.session, this, message, promise); + if (message) { + log.verbose('network', + `Client ${this.id.substr(0, 8)} queue id:${message.id.substr(0, 8)}, type:${message.payload.type}`); + log.verbose('network-content', JSON.stringify(message, (key, value) => filterEmpty(value))); + this.queuedMessages.push({ message, promise, timeoutSeconds }); + } + } + + public filterQueuedMessages(callbackfn: (value: QueuedMessage) => any) { + const filteredMessages: QueuedMessage[] = []; + this._queuedMessages = this._queuedMessages.filter((value) => { + const allow = callbackfn(value); + if (allow) { + filteredMessages.push(value); + } + return !allow; + }); + return filteredMessages; + } +} diff --git a/packages/sdk/src/adapters/multipeer/clientDesyncPreprocessor.ts b/packages/sdk/src/internal/adapters/multipeer/clientDesyncPreprocessor.ts similarity index 79% rename from packages/sdk/src/adapters/multipeer/clientDesyncPreprocessor.ts rename to packages/sdk/src/internal/adapters/multipeer/clientDesyncPreprocessor.ts index 2c6c13aa8..63e16775a 100644 --- a/packages/sdk/src/adapters/multipeer/clientDesyncPreprocessor.ts +++ b/packages/sdk/src/internal/adapters/multipeer/clientDesyncPreprocessor.ts @@ -3,17 +3,21 @@ * Licensed under the MIT License. */ -import { Client, MissingRule, Rules } from '.'; -import { Message } from '../..'; -import { Middleware } from '../../protocols'; -import { UserJoined } from '../../types/network/payloads'; -import { ExportedPromise } from '../../utils/exportedPromise'; +import { + Client, + ExportedPromise, + Message, + MissingRule, + Payloads, + Protocols, + Rules +} from '../../../internal'; /** * Filter user-exclusive actors to a queue, then flush them after user-join * @hidden */ -export class ClientDesyncPreprocessor implements Middleware { +export class ClientDesyncPreprocessor implements Protocols.Middleware { constructor(private client: Client) { } /** @hidden */ public beforeSend(message: Message, promise?: ExportedPromise): Message { @@ -35,7 +39,7 @@ export class ClientDesyncPreprocessor implements Middleware { /** @hidden */ public beforeRecv(message: Message): Message { if (message.payload.type === 'user-joined') { - const userJoin = message.payload as UserJoined; + const userJoin = message.payload as Payloads.UserJoined; this.client.userId = userJoin.user.id; while (this.client.userExclusiveMessages.length > 0) { const queuedMsg = this.client.userExclusiveMessages.splice(0, 1)[0]; diff --git a/packages/sdk/src/adapters/multipeer/index.ts b/packages/sdk/src/internal/adapters/multipeer/index.ts similarity index 89% rename from packages/sdk/src/adapters/multipeer/index.ts rename to packages/sdk/src/internal/adapters/multipeer/index.ts index 5f5e8981a..2f6977fc5 100644 --- a/packages/sdk/src/adapters/multipeer/index.ts +++ b/packages/sdk/src/internal/adapters/multipeer/index.ts @@ -1,14 +1,14 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -export * from './adapter'; -export * from './session'; -export * from './client'; -export * from './syncActor'; -export * from './syncAnimation'; -export * from './syncAsset'; -export * from './protocols'; -export * from './rules'; -export * from './clientDesyncPreprocessor'; +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +export * from './protocols'; + +export * from './client'; +export * from './clientDesyncPreprocessor'; +export * from './rules'; +export * from './session'; +export * from './syncActor'; +export * from './syncAnimation'; +export * from './syncAsset'; diff --git a/packages/sdk/src/adapters/multipeer/protocols/clientExecution.ts b/packages/sdk/src/internal/adapters/multipeer/protocols/clientExecution.ts similarity index 87% rename from packages/sdk/src/adapters/multipeer/protocols/clientExecution.ts rename to packages/sdk/src/internal/adapters/multipeer/protocols/clientExecution.ts index d9c7166bd..0c1d86518 100644 --- a/packages/sdk/src/adapters/multipeer/protocols/clientExecution.ts +++ b/packages/sdk/src/internal/adapters/multipeer/protocols/clientExecution.ts @@ -1,82 +1,86 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -import { Client, ClientDesyncPreprocessor } from '..'; -import { MissingRule, Rules } from '..'; -import { Message } from '../../..'; -import * as Protocols from '../../../protocols'; -import { ExportedPromise } from '../../../utils/exportedPromise'; - -/** - * @hidden - * Class for routing messages between the client and the session - */ -export class ClientExecution extends Protocols.Protocol implements Protocols.Middleware { - private heartbeat: Protocols.Heartbeat; - private heartbeatTimer: NodeJS.Timer; - - /** @override */ - public get name(): string { return `${this.constructor.name} client ${this.client.id.substr(0, 8)}`; } - - constructor(private client: Client) { - super(client.conn); - this.heartbeat = new Protocols.Heartbeat(this); - this.beforeRecv = this.beforeRecv.bind(this); - // Behave like a server-side endpoint (send heartbeats, measure connection quality) - this.use(new Protocols.ServerPreprocessing()); - // Filter user-exclusive actors - this.use(new ClientDesyncPreprocessor(client)); - // Use middleware to pipe client messages to the session. - this.use(this); - } - - /** @override */ - public sendMessage(message: Message, promise?: ExportedPromise, timeoutSeconds?: number) { - // Apply timeout to messages going to the client. - const rule = Rules[message.payload.type] || MissingRule; - super.sendMessage(message, promise, rule.client.timeoutSeconds); - } - - public startListening() { - super.startListening(); - if (!this.heartbeatTimer) { - // Periodically measure connection latency. - this.heartbeatTimer = this.setHeartbeatTimer(); - } - } - - public stopListening() { - clearTimeout(this.heartbeatTimer); - this.heartbeatTimer = undefined; - super.stopListening(); - } - - private setHeartbeatTimer(): NodeJS.Timer { - return setTimeout(async () => { - if (this.heartbeatTimer) { - try { - await this.heartbeat.send(); - this.heartbeatTimer = this.setHeartbeatTimer(); - } catch { - this.client.leave(); - this.resolve(); - } - } - // Irregular heartbeats are a good thing in this instance. - }, 1000 * (5 + 5 * Math.random())); - } - - public beforeRecv = (message: Message): Message => { - if (this.promises.has(message.replyToId)) { - // If we have a queued promise for this message, let it through - return message; - } else { - // Notify listeners we received a message. - this.emit('recv', message); - // Cancel the message - return undefined; - } - }; -} +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { + Client, + ClientDesyncPreprocessor, + ExportedPromise, + Message, + MissingRule, + Protocols, + Rules +} from '../../..'; + +/** + * @hidden + * Class for routing messages between the client and the session + */ +export class ClientExecution extends Protocols.Protocol implements Protocols.Middleware { + private heartbeat: Protocols.Heartbeat; + private heartbeatTimer: NodeJS.Timer; + + /** @override */ + public get name(): string { return `${this.constructor.name} client ${this.client.id.substr(0, 8)}`; } + + constructor(private client: Client) { + super(client.conn); + this.heartbeat = new Protocols.Heartbeat(this); + this.beforeRecv = this.beforeRecv.bind(this); + // Behave like a server-side endpoint (send heartbeats, measure connection quality) + this.use(new Protocols.ServerPreprocessing()); + // Filter user-exclusive actors + this.use(new ClientDesyncPreprocessor(client)); + // Use middleware to pipe client messages to the session. + this.use(this); + } + + /** @override */ + public sendMessage(message: Message, promise?: ExportedPromise, timeoutSeconds?: number) { + // Apply timeout to messages going to the client. + const rule = Rules[message.payload.type] || MissingRule; + super.sendMessage(message, promise, rule.client.timeoutSeconds); + } + + public startListening() { + super.startListening(); + if (!this.heartbeatTimer) { + // Periodically measure connection latency. + this.heartbeatTimer = this.setHeartbeatTimer(); + } + } + + public stopListening() { + clearTimeout(this.heartbeatTimer); + this.heartbeatTimer = undefined; + super.stopListening(); + } + + private setHeartbeatTimer(): NodeJS.Timer { + return setTimeout(async () => { + if (this.heartbeatTimer) { + try { + await this.heartbeat.send(); + this.heartbeatTimer = this.setHeartbeatTimer(); + } catch { + this.client.leave(); + this.resolve(); + } + } + // Irregular heartbeats are a good thing in this instance. + }, 1000 * (5 + 5 * Math.random())); + } + + public beforeRecv = (message: Message): Message => { + if (this.promises.has(message.replyToId)) { + // If we have a queued promise for this message, let it through + return message; + } else { + // Notify listeners we received a message. + this.emit('recv', message); + // Cancel the message + return undefined; + } + }; +} diff --git a/packages/sdk/src/adapters/multipeer/protocols/clientHandshake.ts b/packages/sdk/src/internal/adapters/multipeer/protocols/clientHandshake.ts similarity index 63% rename from packages/sdk/src/adapters/multipeer/protocols/clientHandshake.ts rename to packages/sdk/src/internal/adapters/multipeer/protocols/clientHandshake.ts index 50d4998bd..368a48d5e 100644 --- a/packages/sdk/src/adapters/multipeer/protocols/clientHandshake.ts +++ b/packages/sdk/src/internal/adapters/multipeer/protocols/clientHandshake.ts @@ -1,30 +1,33 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -import { MissingRule, Rules } from '..'; -import { Client } from '../../..'; -import { Message } from '../../..'; -import { Handshake } from '../../../protocols/handshake'; -import { OperatingModel } from '../../../types/network/operatingModel'; -import { ExportedPromise } from '../../../utils/exportedPromise'; - -/** - * @hidden - */ -export class ClientHandshake extends Handshake { - /** @override */ - public get name(): string { return `${this.constructor.name} client ${this.client.id.substr(0, 8)}`; } - - constructor(private client: Client, sessionId: string) { - super(client.conn, sessionId, OperatingModel.PeerAuthoritative); - } - - /** @override */ - public sendMessage(message: Message, promise?: ExportedPromise, timeoutSeconds?: number) { - // Apply timeout to messages going to the client. - const rule = Rules[message.payload.type] || MissingRule; - super.sendMessage(message, promise, rule.client.timeoutSeconds); - } -} +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { + Client, + ExportedPromise, + Message, + MissingRule, + OperatingModel, + Protocols, + Rules +} from '../../..'; + +/** + * @hidden + */ +export class ClientHandshake extends Protocols.Handshake { + /** @override */ + public get name(): string { return `${this.constructor.name} client ${this.client.id.substr(0, 8)}`; } + + constructor(private client: Client, sessionId: string) { + super(client.conn, sessionId, OperatingModel.PeerAuthoritative); + } + + /** @override */ + public sendMessage(message: Message, promise?: ExportedPromise, timeoutSeconds?: number) { + // Apply timeout to messages going to the client. + const rule = Rules[message.payload.type] || MissingRule; + super.sendMessage(message, promise, rule.client.timeoutSeconds); + } +} diff --git a/packages/sdk/src/adapters/multipeer/protocols/clientStartup.ts b/packages/sdk/src/internal/adapters/multipeer/protocols/clientStartup.ts similarity index 83% rename from packages/sdk/src/adapters/multipeer/protocols/clientStartup.ts rename to packages/sdk/src/internal/adapters/multipeer/protocols/clientStartup.ts index e51fe3b3e..66e92f680 100644 --- a/packages/sdk/src/adapters/multipeer/protocols/clientStartup.ts +++ b/packages/sdk/src/internal/adapters/multipeer/protocols/clientStartup.ts @@ -3,12 +3,15 @@ * Licensed under the MIT License. */ -import { Client } from '..'; -import { MissingRule, Rules } from '..'; -import { Message } from '../../..'; -import * as Protocols from '../../../protocols'; -import * as Payloads from '../../../types/network/payloads'; -import { ExportedPromise } from '../../../utils/exportedPromise'; +import { + Client, + ExportedPromise, + Message, + MissingRule, + Payloads, + Protocols, + Rules +} from '../../..'; export class ClientStartup extends Protocols.Protocol { /** @override */ diff --git a/packages/sdk/src/adapters/multipeer/protocols/clientSync.ts b/packages/sdk/src/internal/adapters/multipeer/protocols/clientSync.ts similarity index 94% rename from packages/sdk/src/adapters/multipeer/protocols/clientSync.ts rename to packages/sdk/src/internal/adapters/multipeer/protocols/clientSync.ts index ec5a4fd72..2296ee707 100644 --- a/packages/sdk/src/adapters/multipeer/protocols/clientSync.ts +++ b/packages/sdk/src/internal/adapters/multipeer/protocols/clientSync.ts @@ -1,357 +1,366 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -import { Client, ClientDesyncPreprocessor, MissingRule, Rules, SyncActor } from '..'; -import { Message, newGuid } from '../../..'; -import { log } from '../../../log'; -import * as Protocols from '../../../protocols'; -import * as Payloads from '../../../types/network/payloads'; -import { ExportedPromise } from '../../../utils/exportedPromise'; - -/** - * @hidden - */ -export type SynchronizationStage = - 'always' | - 'load-assets' | - 'create-actors' | - 'active-media-instances' | - 'create-animations' | - 'sync-animations' | - 'set-behaviors' | - 'never'; - -/** - * @hidden - * Synchronizes application state with a client. - */ -export class ClientSync extends Protocols.Protocol { - private inProgressStages: SynchronizationStage[] = []; - private completedStages: SynchronizationStage[] = []; - - // The order of synchronization stages. - private sequence: SynchronizationStage[] = [ - 'load-assets', - 'create-actors', - 'active-media-instances', - 'set-behaviors', - 'create-animations', - 'sync-animations', - ]; - - /** @override */ - public get name(): string { return `${this.constructor.name} client ${this.client.id.substr(0, 8)}`; } - - constructor(private client: Client) { - super(client.conn); - // Behave like a server-side endpoint (send heartbeats, measure connection quality) - this.use(new Protocols.ServerPreprocessing()); - // Queue up user-exclusive messages until the user has joined - this.use(new ClientDesyncPreprocessor(client)); - } - - /** - * @override - * Handle the outgoing message according to the synchronization rules specified for this payload. - */ - public sendMessage(message: Message, promise?: ExportedPromise, timeoutSeconds?: number) { - message.id = message.id ?? newGuid(); - const handling = this.handlingForMessage(message); - switch (handling) { - case 'allow': { - super.sendMessage(message, promise, timeoutSeconds); - break; - } - case 'queue': { - this.client.queueMessage(message, promise, timeoutSeconds); - break; - } - case 'ignore': { - break; - } - case 'error': { - log.error('network', `[ERROR] ${this.name}: ` + - `Invalid message for send during synchronization stage: ${message.payload.type}. ` + - `In progress: ${this.inProgressStages.join(',')}. ` + - `Complete: ${this.completedStages.join(',')}.`); - } - } - } - - /** @override */ - protected missingPromiseForReplyMessage(message: Message) { - // Ignore. Sync protocol receives reply messages for create-* messages, but doesn't queue - // completion promises for them because it doesn't care about when they complete. - } - - private handlingForMessage(message: Message) { - const rule = Rules[message.payload.type] || MissingRule; - let handling = rule.synchronization.before; - if (this.isStageComplete(rule.synchronization.stage)) { - handling = rule.synchronization.after; - } else if (this.isStageInProgress(rule.synchronization.stage)) { - handling = rule.synchronization.during; - } - return handling; - } - - private isStageComplete(stage: SynchronizationStage) { - return this.completedStages.includes(stage); - } - - private isStageInProgress(stage: SynchronizationStage) { - return this.inProgressStages.includes(stage); - } - - private beginStage(stage: SynchronizationStage) { - log.debug('network', `${this.name} - begin stage '${stage}'`); - this.inProgressStages = [...this.inProgressStages, stage]; - } - - private completeStage(stage: SynchronizationStage) { - log.debug('network', `${this.name} - complete stage '${stage}'`); - this.inProgressStages = this.inProgressStages.filter(item => item !== stage); - this.completedStages = [...this.completedStages, stage]; - } - - private async executeStage(stage: SynchronizationStage) { - const handler = (this as any)[`stage:${stage}`]; - if (handler) { - await handler(); // Allow exception to propagate. - } else { - log.error('network', `[ERROR] ${this.name}: No handler for stage ${stage}!`); - } - } - - /** - * @override - */ - public async run() { - try { - this.startListening(); - this.beginStage('always'); - if (this.client.session.peerAuthoritative) { - // Run all the synchronization stages. - for (const stage of this.sequence) { - this.beginStage(stage); - await this.executeStage(stage); - this.completeStage(stage); - await this.sendQueuedMessages(); - } - } - this.completeStage('always'); - // Notify the client we're done synchronizing. - this.sendPayload({ type: 'sync-complete' } as Payloads.SyncComplete); - // Send all remaining queued messages. - await this.sendQueuedMessages(); - this.resolve(); - } catch (e) { - this.reject(e); - } - } - - /** - * @hidden - * Driver for the `load-assets` synchronization stage. - */ - public 'stage:load-assets' = () => { - // Send all cached asset creation messages. - for (const creator of this.client.session.assetCreators) { - this.sendMessage(creator); - } - - // Send all cached asset-update messages. - for (const update of this.client.session.assets.map(a => a.update).filter(x => !!x)) { - this.sendMessage(update); - } - }; - - /** - * @hidden - * Driver for the `create-actors` synchronization stage. - */ - public 'stage:create-actors' = () => { - // Sync cached create-actor hierarchies, starting at roots. - this.client.session.rootActors.map( - syncActor => this.createActorRecursive(syncActor)); - }; - - /** - * @hidden - * Driver for the `set-behaviors` synchronization stage. - */ - public 'stage:set-behaviors' = () => { - // Send all cached set-behavior messages. - this.client.session.actors.map(syncActor => this.createActorBehavior(syncActor)); - }; - - /** - * @hidden - * Driver for the `active-media-instances` synchronization stage. - */ - public 'stage:active-media-instances' = () => { - // Send all cached set-behavior messages. - this.client.session.actors.map(syncActor => this.activeMediaInstances(syncActor)); - }; - /** - * @hidden - * Driver for the `create-animations` synchronization stage. - */ - public 'stage:create-animations' = () => { - // Send all cached interpolate-actor and create-animation messages. - for (const syncActor of this.client.session.actors) { - this.createActorInterpolations(syncActor); - this.createActorAnimations(syncActor); - } - // send all managed create-animation messages - for (const message of this.client.session.animationCreators) { - if (message.payload.type === 'create-animation') { - super.sendMessage(message); - } - } - }; - - /** - * @hidden - * Driver for the `sync-animations` synchronization stage. - */ - public 'stage:sync-animations' = () => { - // sync new-style animations - for (const anim of this.client.session.animations) { - if (anim.update) { - super.sendMessage(anim.update); - } - } - - // sync legacy animations - const authoritativeClient = this.client.session.authoritativeClient; - if (!authoritativeClient) { - return Promise.resolve(); - } - return new Promise((resolve, reject) => { - // Request the current state of all animations from the authoritative client. - // TODO: Improve this (don't rely on a peer). - authoritativeClient.sendPayload({ - type: 'sync-animations', - } as Payloads.SyncAnimations, { - resolve: (payload: Payloads.SyncAnimations) => { - // We've received the sync-animations payload from the authoritative - // client, now pass it to the joining client. - for (const animationState of payload.animationStates) { - // Account for latency on the authoritative peer's connection. - animationState.state.time += authoritativeClient.conn.quality.latencyMs.value / 2000; - // Account for latency on the joining peer's connection. - animationState.state.time += this.conn.quality.latencyMs.value / 2000; - } - // Pass with an empty reply handler to account for an edge case that will go away once - // animation synchronization is refactored. - super.sendPayload(payload); - resolve(); - }, reject - }); - }); - }; - - private createActorRecursive(actor: Partial) { - // Start creating this actor and its creatable children. - this.createActor(actor); // Allow exception to propagate. - // const children = this.client.session.childrenOf(actor.created.message.payload.actor.id); - const children = this.client.session.creatableChildrenOf(actor.initialization.message.payload.actor.id); - if (children.length) { - for (const child of children) { - this.createActorRecursive(child); - } - } - } - - private createActorBehavior(actor: Partial) { - if (actor.behavior) { - super.sendPayload({ - type: 'set-behavior', - behaviorType: actor.behavior, - actorId: actor.actorId - } as Payloads.SetBehavior); - } - } - - private createActor(actor: Partial) { - if (actor.initialization && actor.initialization.message.payload.type) { - return this.sendMessage(actor.initialization.message); - } - } - - private activeMediaInstances(actor: Partial) { - (actor.activeMediaInstances || []) - .map(activeMediaInstance => { - // TODO This sound tweaking should ideally be done on the client, because then it can consider the - // time it takes for packet to arrive. This is needed for optimal timing . - const targetTime = Date.now() / 1000.0; - if (activeMediaInstance.expirationTime !== undefined && - targetTime > activeMediaInstance.expirationTime) { - // non-looping mediainstance has completed, so ignore it - return undefined; - } - if (activeMediaInstance.message.payload.options.paused !== true) { - let timeOffset = (targetTime - activeMediaInstance.basisTime); - if (activeMediaInstance.message.payload.options.pitch !== undefined) { - timeOffset *= Math.pow(2.0, (activeMediaInstance.message.payload.options.pitch / 12.0)); - } - if (activeMediaInstance.message.payload.options.time === undefined) { - activeMediaInstance.message.payload.options.time = 0.0; - } - activeMediaInstance.message.payload.options.time += timeOffset; - activeMediaInstance.basisTime = targetTime; - } - return this.sendMessage(activeMediaInstance.message); - }); - } - - private createActorAnimations(actor: Partial) { - (actor.createdAnimations || []) - .map(createdAnimation => this.sendMessage(createdAnimation.message)); - } - - private createActorInterpolations(actor: Partial) { - for (let activeInterpolation of actor.activeInterpolations || []) { - // Don't start the interpolations on the new client. They will be started in the syncAnimations phase. - activeInterpolation = { - ...activeInterpolation, - enabled: false - }; - super.sendPayload(activeInterpolation); - } - } - - private sendAndExpectResponse(message: Message) { - return new Promise((resolve, reject) => { - super.sendMessage(message, { - resolve: (replyPayload: any, replyMessage: Message) => { - this.client.session.conn.send(replyMessage); - resolve(replyPayload); - }, reject - }); - }); - } - - public async sendQueuedMessages() { - // 1. Get the subset of queued messages that can be sent now. - // 2. Send the messages and wait for expected replies. - // 3. Repeat until no more messages to send. - do { - const queuedMessages = this.client.filterQueuedMessages((queuedMessage) => { - const message = queuedMessage.message; - const handling = this.handlingForMessage(message); - return handling === 'allow'; - }); - if (!queuedMessages.length) { - break; - } - for (const queuedMessage of queuedMessages) { - this.sendMessage(queuedMessage.message, queuedMessage.promise, queuedMessage.timeoutSeconds); - } - await this.drainPromises(); - } while (true); // eslint-disable-line no-constant-condition - } -} +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { + log, + newGuid +} from '../../../..'; +import { + Client, + ClientDesyncPreprocessor, + ExportedPromise, + Message, + MissingRule, + Payloads, + Protocols, + Rules, + SyncActor +} from '../../../../internal'; + +/** + * @hidden + */ +export type SynchronizationStage = + 'always' | + 'load-assets' | + 'create-actors' | + 'active-media-instances' | + 'create-animations' | + 'sync-animations' | + 'set-behaviors' | + 'never'; + +/** + * @hidden + * Synchronizes application state with a client. + */ +export class ClientSync extends Protocols.Protocol { + private inProgressStages: SynchronizationStage[] = []; + private completedStages: SynchronizationStage[] = []; + + // The order of synchronization stages. + private sequence: SynchronizationStage[] = [ + 'load-assets', + 'create-actors', + 'active-media-instances', + 'set-behaviors', + 'create-animations', + 'sync-animations', + ]; + + /** @override */ + public get name(): string { return `${this.constructor.name} client ${this.client.id.substr(0, 8)}`; } + + constructor(private client: Client) { + super(client.conn); + // Behave like a server-side endpoint (send heartbeats, measure connection quality) + this.use(new Protocols.ServerPreprocessing()); + // Queue up user-exclusive messages until the user has joined + this.use(new ClientDesyncPreprocessor(client)); + } + + /** + * @override + * Handle the outgoing message according to the synchronization rules specified for this payload. + */ + public sendMessage(message: Message, promise?: ExportedPromise, timeoutSeconds?: number) { + message.id = message.id ?? newGuid(); + const handling = this.handlingForMessage(message); + switch (handling) { + case 'allow': { + super.sendMessage(message, promise, timeoutSeconds); + break; + } + case 'queue': { + this.client.queueMessage(message, promise, timeoutSeconds); + break; + } + case 'ignore': { + break; + } + case 'error': { + log.error('network', `[ERROR] ${this.name}: ` + + `Invalid message for send during synchronization stage: ${message.payload.type}. ` + + `In progress: ${this.inProgressStages.join(',')}. ` + + `Complete: ${this.completedStages.join(',')}.`); + } + } + } + + /** @override */ + protected missingPromiseForReplyMessage(message: Message) { + // Ignore. Sync protocol receives reply messages for create-* messages, but doesn't queue + // completion promises for them because it doesn't care about when they complete. + } + + private handlingForMessage(message: Message) { + const rule = Rules[message.payload.type] || MissingRule; + let handling = rule.synchronization.before; + if (this.isStageComplete(rule.synchronization.stage)) { + handling = rule.synchronization.after; + } else if (this.isStageInProgress(rule.synchronization.stage)) { + handling = rule.synchronization.during; + } + return handling; + } + + private isStageComplete(stage: SynchronizationStage) { + return this.completedStages.includes(stage); + } + + private isStageInProgress(stage: SynchronizationStage) { + return this.inProgressStages.includes(stage); + } + + private beginStage(stage: SynchronizationStage) { + log.debug('network', `${this.name} - begin stage '${stage}'`); + this.inProgressStages = [...this.inProgressStages, stage]; + } + + private completeStage(stage: SynchronizationStage) { + log.debug('network', `${this.name} - complete stage '${stage}'`); + this.inProgressStages = this.inProgressStages.filter(item => item !== stage); + this.completedStages = [...this.completedStages, stage]; + } + + private async executeStage(stage: SynchronizationStage) { + const handler = (this as any)[`stage:${stage}`]; + if (handler) { + await handler(); // Allow exception to propagate. + } else { + log.error('network', `[ERROR] ${this.name}: No handler for stage ${stage}!`); + } + } + + /** + * @override + */ + public async run() { + try { + this.startListening(); + this.beginStage('always'); + if (this.client.session.peerAuthoritative) { + // Run all the synchronization stages. + for (const stage of this.sequence) { + this.beginStage(stage); + await this.executeStage(stage); + this.completeStage(stage); + await this.sendQueuedMessages(); + } + } + this.completeStage('always'); + // Notify the client we're done synchronizing. + this.sendPayload({ type: 'sync-complete' } as Payloads.SyncComplete); + // Send all remaining queued messages. + await this.sendQueuedMessages(); + this.resolve(); + } catch (e) { + this.reject(e); + } + } + + /** + * @hidden + * Driver for the `load-assets` synchronization stage. + */ + public 'stage:load-assets' = () => { + // Send all cached asset creation messages. + for (const creator of this.client.session.assetCreators) { + this.sendMessage(creator); + } + + // Send all cached asset-update messages. + for (const update of this.client.session.assets.map(a => a.update).filter(x => !!x)) { + this.sendMessage(update); + } + }; + + /** + * @hidden + * Driver for the `create-actors` synchronization stage. + */ + public 'stage:create-actors' = () => { + // Sync cached create-actor hierarchies, starting at roots. + this.client.session.rootActors.map( + syncActor => this.createActorRecursive(syncActor)); + }; + + /** + * @hidden + * Driver for the `set-behaviors` synchronization stage. + */ + public 'stage:set-behaviors' = () => { + // Send all cached set-behavior messages. + this.client.session.actors.map(syncActor => this.createActorBehavior(syncActor)); + }; + + /** + * @hidden + * Driver for the `active-media-instances` synchronization stage. + */ + public 'stage:active-media-instances' = () => { + // Send all cached set-behavior messages. + this.client.session.actors.map(syncActor => this.activeMediaInstances(syncActor)); + }; + /** + * @hidden + * Driver for the `create-animations` synchronization stage. + */ + public 'stage:create-animations' = () => { + // Send all cached interpolate-actor and create-animation messages. + for (const syncActor of this.client.session.actors) { + this.createActorInterpolations(syncActor); + this.createActorAnimations(syncActor); + } + // send all managed create-animation messages + for (const message of this.client.session.animationCreators) { + if (message.payload.type === 'create-animation') { + super.sendMessage(message); + } + } + }; + + /** + * @hidden + * Driver for the `sync-animations` synchronization stage. + */ + public 'stage:sync-animations' = () => { + // sync new-style animations + for (const anim of this.client.session.animations) { + if (anim.update) { + super.sendMessage(anim.update); + } + } + + // sync legacy animations + const authoritativeClient = this.client.session.authoritativeClient; + if (!authoritativeClient) { + return Promise.resolve(); + } + return new Promise((resolve, reject) => { + // Request the current state of all animations from the authoritative client. + // TODO: Improve this (don't rely on a peer). + authoritativeClient.sendPayload({ + type: 'sync-animations', + } as Payloads.SyncAnimations, { + resolve: (payload: Payloads.SyncAnimations) => { + // We've received the sync-animations payload from the authoritative + // client, now pass it to the joining client. + for (const animationState of payload.animationStates) { + // Account for latency on the authoritative peer's connection. + animationState.state.time += authoritativeClient.conn.quality.latencyMs.value / 2000; + // Account for latency on the joining peer's connection. + animationState.state.time += this.conn.quality.latencyMs.value / 2000; + } + // Pass with an empty reply handler to account for an edge case that will go away once + // animation synchronization is refactored. + super.sendPayload(payload); + resolve(); + }, reject + }); + }); + }; + + private createActorRecursive(actor: Partial) { + // Start creating this actor and its creatable children. + this.createActor(actor); // Allow exception to propagate. + // const children = this.client.session.childrenOf(actor.created.message.payload.actor.id); + const children = this.client.session.creatableChildrenOf(actor.initialization.message.payload.actor.id); + if (children.length) { + for (const child of children) { + this.createActorRecursive(child); + } + } + } + + private createActorBehavior(actor: Partial) { + if (actor.behavior) { + super.sendPayload({ + type: 'set-behavior', + behaviorType: actor.behavior, + actorId: actor.actorId + } as Payloads.SetBehavior); + } + } + + private createActor(actor: Partial) { + if (actor.initialization && actor.initialization.message.payload.type) { + return this.sendMessage(actor.initialization.message); + } + } + + private activeMediaInstances(actor: Partial) { + (actor.activeMediaInstances || []) + .map(activeMediaInstance => { + // TODO This sound tweaking should ideally be done on the client, because then it can consider the + // time it takes for packet to arrive. This is needed for optimal timing . + const targetTime = Date.now() / 1000.0; + if (activeMediaInstance.expirationTime !== undefined && + targetTime > activeMediaInstance.expirationTime) { + // non-looping mediainstance has completed, so ignore it + return undefined; + } + if (activeMediaInstance.message.payload.options.paused !== true) { + let timeOffset = (targetTime - activeMediaInstance.basisTime); + if (activeMediaInstance.message.payload.options.pitch !== undefined) { + timeOffset *= Math.pow(2.0, (activeMediaInstance.message.payload.options.pitch / 12.0)); + } + if (activeMediaInstance.message.payload.options.time === undefined) { + activeMediaInstance.message.payload.options.time = 0.0; + } + activeMediaInstance.message.payload.options.time += timeOffset; + activeMediaInstance.basisTime = targetTime; + } + return this.sendMessage(activeMediaInstance.message); + }); + } + + private createActorAnimations(actor: Partial) { + (actor.createdAnimations || []) + .map(createdAnimation => this.sendMessage(createdAnimation.message)); + } + + private createActorInterpolations(actor: Partial) { + for (let activeInterpolation of actor.activeInterpolations || []) { + // Don't start the interpolations on the new client. They will be started in the syncAnimations phase. + activeInterpolation = { + ...activeInterpolation, + enabled: false + }; + super.sendPayload(activeInterpolation); + } + } + + private sendAndExpectResponse(message: Message) { + return new Promise((resolve, reject) => { + super.sendMessage(message, { + resolve: (replyPayload: any, replyMessage: Message) => { + this.client.session.conn.send(replyMessage); + resolve(replyPayload); + }, reject + }); + }); + } + + public async sendQueuedMessages() { + // 1. Get the subset of queued messages that can be sent now. + // 2. Send the messages and wait for expected replies. + // 3. Repeat until no more messages to send. + do { + const queuedMessages = this.client.filterQueuedMessages((queuedMessage) => { + const message = queuedMessage.message; + const handling = this.handlingForMessage(message); + return handling === 'allow'; + }); + if (!queuedMessages.length) { + break; + } + for (const queuedMessage of queuedMessages) { + this.sendMessage(queuedMessage.message, queuedMessage.promise, queuedMessage.timeoutSeconds); + } + await this.drainPromises(); + } while (true); // eslint-disable-line no-constant-condition + } +} diff --git a/packages/sdk/src/adapters/multipeer/protocols/index.ts b/packages/sdk/src/internal/adapters/multipeer/protocols/index.ts similarity index 96% rename from packages/sdk/src/adapters/multipeer/protocols/index.ts rename to packages/sdk/src/internal/adapters/multipeer/protocols/index.ts index 89ea52642..71be95018 100644 --- a/packages/sdk/src/adapters/multipeer/protocols/index.ts +++ b/packages/sdk/src/internal/adapters/multipeer/protocols/index.ts @@ -1,12 +1,12 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -export * from './clientHandshake'; -export * from './clientStartup'; -export * from './clientExecution'; -export * from './clientSync'; -export * from './sessionHandshake'; -export * from './sessionExecution'; -export * from './sessionSync'; +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +export * from './clientExecution'; +export * from './clientHandshake'; +export * from './clientStartup'; +export * from './clientSync'; +export * from './sessionExecution'; +export * from './sessionHandshake'; +export * from './sessionSync'; diff --git a/packages/sdk/src/adapters/multipeer/protocols/sessionExecution.ts b/packages/sdk/src/internal/adapters/multipeer/protocols/sessionExecution.ts similarity index 85% rename from packages/sdk/src/adapters/multipeer/protocols/sessionExecution.ts rename to packages/sdk/src/internal/adapters/multipeer/protocols/sessionExecution.ts index a5bd80abe..fbf5cc86f 100644 --- a/packages/sdk/src/adapters/multipeer/protocols/sessionExecution.ts +++ b/packages/sdk/src/internal/adapters/multipeer/protocols/sessionExecution.ts @@ -1,31 +1,29 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -import { Session } from '..'; -import { Message } from '../../..'; -import * as Protocols from '../../../protocols'; - -/** - * @hidden - * Class for routing messages from the app over to the session - */ -export class SessionExecution extends Protocols.Protocol implements Protocols.Middleware { - constructor(private session: Session) { - super(session.conn); - this.beforeRecv = this.beforeRecv.bind(this); - // Behave like a client-side endpoint (record latency, respond to heartbeats). - this.use(new Protocols.ClientPreprocessing(this)); - // Use middleware to take incoming messages from the app and pipe them to the session. - this.use(this); - } - - /** @private */ - public beforeRecv = (message: Message): Message => { - // Notify listeners we received a message from the application - this.emit('recv', message); - // Cancel the message - return undefined; - }; -} +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { Message, Protocols, Session } from '../../../../internal'; + +/** + * @hidden + * Class for routing messages from the app over to the session + */ +export class SessionExecution extends Protocols.Protocol implements Protocols.Middleware { + constructor(private session: Session) { + super(session.conn); + this.beforeRecv = this.beforeRecv.bind(this); + // Behave like a client-side endpoint (record latency, respond to heartbeats). + this.use(new Protocols.ClientPreprocessing(this)); + // Use middleware to take incoming messages from the app and pipe them to the session. + this.use(this); + } + + /** @private */ + public beforeRecv = (message: Message): Message => { + // Notify listeners we received a message from the application + this.emit('recv', message); + // Cancel the message + return undefined; + }; +} diff --git a/packages/sdk/src/adapters/multipeer/protocols/sessionHandshake.ts b/packages/sdk/src/internal/adapters/multipeer/protocols/sessionHandshake.ts similarity index 82% rename from packages/sdk/src/adapters/multipeer/protocols/sessionHandshake.ts rename to packages/sdk/src/internal/adapters/multipeer/protocols/sessionHandshake.ts index b71176eab..a541bfa84 100644 --- a/packages/sdk/src/adapters/multipeer/protocols/sessionHandshake.ts +++ b/packages/sdk/src/internal/adapters/multipeer/protocols/sessionHandshake.ts @@ -1,32 +1,30 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -import { Session } from '..'; -import * as Protocols from '../../../protocols'; -import * as Payloads from '../../../types/network/payloads'; - -/** - * @hidden - * Protocol for handling handshake with the app instance (Session is a client of App) - */ -export class SessionHandshake extends Protocols.Protocol { - constructor(session: Session) { - super(session.conn); - // Behave like a client-side endpoint (record latency, respond to heartbeats). - this.use(new Protocols.ClientPreprocessing(this)); - } - - /** @override */ - public startListening() { - super.startListening(); - this.sendPayload({ type: 'handshake' } as Payloads.Handshake); - } - - /** @private */ - public 'recv-handshake-reply' = (payload: Payloads.HandshakeReply) => { - this.sendPayload({ type: 'handshake-complete' } as Payloads.HandshakeComplete); - this.resolve(); - }; -} +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { Payloads, Protocols, Session } from '../../../../internal'; + +/** + * @hidden + * Protocol for handling handshake with the app instance (Session is a client of App) + */ +export class SessionHandshake extends Protocols.Protocol { + constructor(session: Session) { + super(session.conn); + // Behave like a client-side endpoint (record latency, respond to heartbeats). + this.use(new Protocols.ClientPreprocessing(this)); + } + + /** @override */ + public startListening() { + super.startListening(); + this.sendPayload({ type: 'handshake' } as Payloads.Handshake); + } + + /** @private */ + public 'recv-handshake-reply' = (payload: Payloads.HandshakeReply) => { + this.sendPayload({ type: 'handshake-complete' } as Payloads.HandshakeComplete); + this.resolve(); + }; +} diff --git a/packages/sdk/src/adapters/multipeer/protocols/sessionSync.ts b/packages/sdk/src/internal/adapters/multipeer/protocols/sessionSync.ts similarity index 81% rename from packages/sdk/src/adapters/multipeer/protocols/sessionSync.ts rename to packages/sdk/src/internal/adapters/multipeer/protocols/sessionSync.ts index 4e36987c1..829a479e5 100644 --- a/packages/sdk/src/adapters/multipeer/protocols/sessionSync.ts +++ b/packages/sdk/src/internal/adapters/multipeer/protocols/sessionSync.ts @@ -1,31 +1,29 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -import { Session } from '..'; -import * as Protocols from '../../../protocols'; -import * as Payloads from '../../../types/network/payloads'; - -/** - * @hidden - * Class to manage the synchronization phase when connecting to the app. There should be no state to synchronize - */ -export class SessionSync extends Protocols.Protocol { - constructor(session: Session) { - super(session.conn); - // Behave like a client-side endpoint (record latency, respond to heartbeats). - this.use(new Protocols.ClientPreprocessing(this)); - } - - /** @override */ - public startListening() { - super.startListening(); - this.sendPayload({ type: 'sync-request' } as Payloads.SyncRequest); - } - - /** @private */ - public 'recv-sync-complete' = (payload: Payloads.SyncComplete) => { - this.resolve(); - }; -} +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { Payloads, Protocols, Session } from '../../../../internal'; + +/** + * @hidden + * Class to manage the synchronization phase when connecting to the app. There should be no state to synchronize + */ +export class SessionSync extends Protocols.Protocol { + constructor(session: Session) { + super(session.conn); + // Behave like a client-side endpoint (record latency, respond to heartbeats). + this.use(new Protocols.ClientPreprocessing(this)); + } + + /** @override */ + public startListening() { + super.startListening(); + this.sendPayload({ type: 'sync-request' } as Payloads.SyncRequest); + } + + /** @private */ + public 'recv-sync-complete' = (payload: Payloads.SyncComplete) => { + this.resolve(); + }; +} diff --git a/packages/sdk/src/adapters/multipeer/rules.ts b/packages/sdk/src/internal/adapters/multipeer/rules.ts similarity index 99% rename from packages/sdk/src/adapters/multipeer/rules.ts rename to packages/sdk/src/internal/adapters/multipeer/rules.ts index ecce3bb8a..17ce2bd76 100644 --- a/packages/sdk/src/adapters/multipeer/rules.ts +++ b/packages/sdk/src/internal/adapters/multipeer/rules.ts @@ -4,11 +4,18 @@ */ import deepmerge from 'deepmerge'; -import { ActiveMediaInstance, Client, Session, SynchronizationStage } from '.'; -import { Guid, MediaCommand, Message, WebSocket } from '../..'; -import { log } from '../../log'; -import * as Payloads from '../../types/network/payloads'; -import { ExportedPromise } from '../../utils/exportedPromise'; + +import { + ActiveMediaInstance, + Client, + ExportedPromise, + Message, + Payloads, + Session, + SynchronizationStage, + WebSocket +} from '../../../internal'; +import { Guid, log, MediaCommand } from '../../..'; /** * @hidden diff --git a/packages/sdk/src/adapters/multipeer/session.ts b/packages/sdk/src/internal/adapters/multipeer/session.ts similarity index 94% rename from packages/sdk/src/adapters/multipeer/session.ts rename to packages/sdk/src/internal/adapters/multipeer/session.ts index 92d291846..9957382b7 100644 --- a/packages/sdk/src/adapters/multipeer/session.ts +++ b/packages/sdk/src/internal/adapters/multipeer/session.ts @@ -1,431 +1,442 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -import deepmerge from 'deepmerge'; -import { EventEmitter } from 'events'; -import { - Client, InitializeActorMessage, MissingRule, Rules, SessionExecution, - SessionHandshake, SessionSync, SyncActor, SyncAnimation, SyncAsset -} from '.'; -import { Connection, EventedConnection, Guid, Message, UserLike } from '../..'; -import { log } from '../../log'; -import * as Protocols from '../../protocols'; -import * as Payloads from '../../types/network/payloads'; - -type AssetCreationMessage = Message; -type AnimationCreationMessage = Message; - -/** - * @hidden - * Class for associating multiple client connections with a single app session. - */ -export class Session extends EventEmitter { - private _clientSet = new Map(); - private _actorSet = new Map>(); - private _assetSet = new Map>(); - private _assetCreatorSet = new Map(); - /** Maps animation IDs to animation sync structs */ - private _animationSet = new Map>(); - /** Maps IDs of messages that can create animations to the messages themselves */ - private _animationCreatorSet = new Map(); - private _userSet = new Map>(); - private _protocol: Protocols.Protocol; - private _disconnect: () => void; - - public get conn() { return this._conn; } - public get sessionId() { return this._sessionId; } - public get protocol() { return this._protocol; } - public get clients() { - return [...this._clientSet.values()].sort((a, b) => a.order - b.order); - } - - public get actors() { return [...this._actorSet.values()]; } - public get assets() { return [...this._assetSet.values()]; } - public get assetCreators() { return [...this._assetCreatorSet.values()]; } - public get animationSet() { return this._animationSet; } - public get animations() { return this._animationSet.values(); } - public get animationCreators() { return this._animationCreatorSet.values(); } - public get actorSet() { return this._actorSet; } - public get assetSet() { return this._assetSet; } - public get assetCreatorSet() { return this._assetCreatorSet; } - public get userSet() { return this._userSet; } - - public get rootActors() { - return this.actors.filter(a => !a.initialization.message.payload.actor.parentId); - } - public get authoritativeClient() { return this.clients.find(client => client.authoritative); } - public get peerAuthoritative() { return this._peerAuthoritative; } - - public client = (clientId: Guid) => this._clientSet.get(clientId); - public actor = (actorId: Guid) => this._actorSet.get(actorId); - public user = (userId: Guid) => this._userSet.get(userId); - public childrenOf = (parentId: Guid) => { - return this.actors.filter(actor => actor.initialization.message.payload.actor.parentId === parentId); - }; - public creatableChildrenOf = (parentId: Guid) => { - return this.actors.filter(actor => - actor.initialization.message.payload.actor.parentId === parentId - && !!actor.initialization.message.payload.type); - }; - - /** - * Creates a new Session instance - */ - constructor(private _conn: Connection, private _sessionId: string, private _peerAuthoritative: boolean) { - super(); - this.recvFromClient = this.recvFromClient.bind(this); - this.recvFromApp = this.recvFromApp.bind(this); - this._disconnect = this.disconnect.bind(this); - this._conn.on('close', this._disconnect); - this._conn.on('error', this._disconnect); - } - - /** - * Performs handshake and sync with the app - */ - public async connect() { - try { - const handshake = this._protocol = new SessionHandshake(this); - await handshake.run(); - - const sync = this._protocol = new SessionSync(this); - await sync.run(); - - const execution = this._protocol = new SessionExecution(this); - execution.on('recv', message => this.recvFromApp(message)); - execution.startListening(); - } catch (e) { - log.error('network', e); - this.disconnect(); - } - } - - public disconnect() { - try { - this._conn.off('close', this._disconnect); - this._conn.off('error', this._disconnect); - this._conn.close(); - this.emit('close'); - } catch { } - } - - /** - * Adds the client to the session - */ - public async join(client: Client) { - try { - this._clientSet.set(client.id, client); - client.on('close', () => this.leave(client.id)); - // Synchronize app state to the client. - await client.join(this); - // Once the client is joined, further messages from the client will be processed by the session - // (as opposed to a protocol class). - client.on('recv', (_: Client, message: Message) => this.recvFromClient(client, message)); - // If we don't have an authoritative client, make this client authoritative. - if (!this.authoritativeClient) { - this.setAuthoritativeClient(client.id); - } - } catch (e) { - log.error('network', e); - this.leave(client.id); - } - } - - /** - * Removes the client from the session - */ - public leave(clientId: Guid) { - try { - const client = this._clientSet.get(clientId); - this._clientSet.delete(clientId); - if (client) { - // If the client is associated with a userId, inform app the user is leaving - if (client.userId) { - this.protocol.sendPayload({ - type: 'user-left', - userId: client.userId - } as Payloads.UserLeft); - } - // Select another client to be the authoritative peer. - // TODO: Make selection criteria more intelligent (look at latency, prefer non-mobile, ...) - if (client.authoritative) { - const nextClient = this.clients.find(c => c.isJoined()); - if (nextClient) { - this.setAuthoritativeClient(nextClient.id); - } - } - } - // If this was the last client then shutdown the session - if (!this.clients.length) { - this._conn.close(); - } - } catch { } - } - - private setAuthoritativeClient(clientId: Guid) { - const newAuthority = this._clientSet.get(clientId); - if (!newAuthority) { - log.error('network', `[ERROR] setAuthoritativeClient: client ${clientId} does not exist.`); - return; - } - const oldAuthority = this.authoritativeClient; - - newAuthority.setAuthoritative(true); - for (const client of this.clients.filter(c => c !== newAuthority)) { - client.setAuthoritative(false); - } - - // forward connection quality metrics - if (this.conn instanceof EventedConnection) { - this.conn.linkConnectionQuality(newAuthority.conn.quality); - } - - // forward network stats from the authoritative peer connection to the app - const toApp = this.conn instanceof EventedConnection ? this.conn : null; - const forwardIncoming = (bytes: number) => toApp.statsTracker.recordIncoming(bytes); - const forwardOutgoing = (bytes: number) => toApp.statsTracker.recordOutgoing(bytes); - const toNewAuthority = newAuthority.conn instanceof EventedConnection ? newAuthority.conn : null; - if (toNewAuthority) { - toNewAuthority.statsTracker.on('incoming', forwardIncoming); - toNewAuthority.statsTracker.on('outgoing', forwardOutgoing); - } - - // turn off old authority - const toOldAuthority = oldAuthority && oldAuthority.conn instanceof EventedConnection - ? oldAuthority.conn : null; - if (toOldAuthority) { - toOldAuthority.statsTracker.off('incoming', forwardIncoming); - toOldAuthority.statsTracker.off('outgoing', forwardOutgoing); - } - } - - private recvFromClient = (client: Client, message: Message) => { - message = this.preprocessFromClient(client, message); - if (message) { - this.sendToApp(message); - } - }; - - private recvFromApp = (message: Message) => { - message = this.preprocessFromApp(message); - if (message) { - this.sendToClients(message); - } - }; - - public preprocessFromApp(message: Message): Message { - const rule = Rules[message.payload.type] || MissingRule; - const beforeReceiveFromApp = rule.session.beforeReceiveFromApp || (() => message); - return beforeReceiveFromApp(this, message); - } - - public preprocessFromClient(client: Client, message: Message): Message { - // Precaution: If we don't recognize this client, drop the message. - if (!this._clientSet.has(client.id)) { - return undefined; - } - if (message.payload && message.payload.type && message.payload.type.length) { - const rule = Rules[message.payload.type] || MissingRule; - const beforeReceiveFromClient = rule.session.beforeReceiveFromClient || (() => message); - message = beforeReceiveFromClient(this, client, message); - } - return message; - } - - public sendToApp(message: Message) { - this.protocol.sendMessage(message); - } - - public sendToClients(message: Message, filterFn?: (value: Client, index: number) => any) { - const clients = this.clients.filter(filterFn || (() => true)); - for (const client of clients) { - client.send({ ...message }); - } - } - - public sendPayloadToClients(payload: Partial, filterFn?: (value: Client, index: number) => any) { - this.sendToClients({ payload }, filterFn); - } - - /** @deprecated */ - public findAnimation(syncActor: Partial, animationName: string) { - return (syncActor.createdAnimations || []).find(item => item.message.payload.animationName === animationName); - } - - /** @deprecated */ - public isAnimating(syncActor: Partial): boolean { - if ((syncActor.createdAnimations || []).some(item => item.enabled)) { - return true; - } - if (syncActor.initialization && - syncActor.initialization.message && - syncActor.initialization.message.payload && - syncActor.initialization.message.payload.actor) { - const parent = this._actorSet.get(syncActor.initialization.message.payload.actor.parentId); - if (parent) { - return this.isAnimating(parent); - } - } - return false; - } - - public cacheInitializeActorMessage(message: InitializeActorMessage) { - let syncActor = this.actorSet.get(message.payload.actor.id); - if (!syncActor) { - const parent = this.actorSet.get(message.payload.actor.parentId); - syncActor = { - actorId: message.payload.actor.id, - exclusiveToUser: parent && parent.exclusiveToUser - || message.payload.actor.exclusiveToUser, - initialization: deepmerge({ message }, {}) - }; - this.actorSet.set(message.payload.actor.id, syncActor); - // update reserved actor init message with something the client can use - } else if (syncActor.initialization.message.payload.type === 'x-reserve-actor') { - // send real init message, but with session's initial actor state - message.payload = { - ...message.payload, - actor: syncActor.initialization.message.payload.actor - }; - // write the merged message back to the session - syncActor.initialization.message = message; - } - } - - public cacheActorUpdateMessage(message: Message) { - const syncActor = this.actorSet.get(message.payload.actor.id); - if (syncActor) { - // Merge the update into the existing actor. - syncActor.initialization.message.payload.actor - = deepmerge(syncActor.initialization.message.payload.actor, message.payload.actor); - - // strip out transform data that wasn't updated - // so it doesn't desync from the updated one - const cacheTransform = syncActor.initialization.message.payload.actor.transform; - const patchTransform = message.payload.actor.transform; - if (patchTransform && patchTransform.app && cacheTransform.local) { - delete cacheTransform.local.position; - delete cacheTransform.local.rotation; - } else if (patchTransform && patchTransform.local) { - delete cacheTransform.app; - } - } - } - - public cacheAssetCreationRequest(message: AssetCreationMessage) { - this.assetCreatorSet.set(message.id, message); - } - - public cacheAssetCreation(assetId: Guid, creatorId: Guid, duration?: number) { - const syncAsset = { - id: assetId, - creatorMessageId: creatorId, - duration - } as Partial; - this.assetSet.set(assetId, syncAsset); - const creator = this.assetCreatorSet.get(creatorId); - - // Updates are cached on send, creates are cached on receive, - // so it's possible something was updated while it was loading. - // Merge those updates into creation once the create comes back. - if (creator.payload.type === 'create-asset' && syncAsset.update) { - creator.payload.definition = deepmerge( - creator.payload.definition, - syncAsset.update.payload.asset - ); - syncAsset.update = undefined; - } - - // update end times on playing media instances with the now-known duration - for (const syncActor of this.actorSet.values()) { - for (const activeMediaInstance of (syncActor.activeMediaInstances || [])) { - if (activeMediaInstance.message.payload.mediaAssetId !== assetId || - activeMediaInstance.message.payload.options.looping === true || - activeMediaInstance.message.payload.options.paused === true || - duration === undefined) { - continue; - } - - let timeRemaining: number = syncAsset.duration; - if (activeMediaInstance.message.payload.options.time !== undefined) { - timeRemaining -= activeMediaInstance.message.payload.options.time; - } - if (activeMediaInstance.message.payload.options.pitch !== undefined) { - timeRemaining /= Math.pow(2.0, - (activeMediaInstance.message.payload.options.pitch / 12.0)); - } - activeMediaInstance.expirationTime = activeMediaInstance.basisTime + timeRemaining; - } - } - } - - public cacheAssetUpdate(update: Message) { - if (!this.assetSet.has(update.payload.asset.id)) { - this.assetSet.set(update.payload.asset.id, { id: update.payload.asset.id }); - } - const syncAsset = this.assetSet.get(update.payload.asset.id); - const creator = this.assetCreatorSet.get(syncAsset.creatorMessageId); - - if (creator && creator.payload.type === 'create-asset') { - // roll update into creation message - creator.payload.definition = deepmerge( - creator.payload.definition, - update.payload.asset); - } else if (syncAsset.update) { - // merge with previous update message - syncAsset.update.payload.asset = deepmerge( - syncAsset.update.payload.asset, - update.payload.asset); - } else { - // just save it - syncAsset.update = update; - } - } - - public cacheAssetUnload(containerId: Guid) { - const creators = this.assetCreators.filter(c => c.payload.containerId === containerId); - for (const creator of creators) { - // un-cache creation message - this.assetCreatorSet.delete(creator.id); - - // un-cache created assets - const assets = this.assets.filter(a => a.creatorMessageId === creator.id); - for (const asset of assets) { - this.assetSet.delete(asset.id); - } - } - } - - public cacheAnimationCreationRequest(payload: AnimationCreationMessage) { - this._animationCreatorSet.set(payload.id, payload); - } - - public cacheAnimationCreation(animId: Guid, creatorId: Guid, duration?: number) { - this._animationSet.set(animId, { - id: animId, - creatorMessageId: creatorId, - update: undefined, - duration - }); - } - - public cacheAnimationUpdate(update: Message) { - let syncAnim = this._animationSet.get(update.payload.animation.id); - if (!syncAnim) { - syncAnim = { id: update.payload.animation.id }; - this._animationSet.set(syncAnim.id, syncAnim); - } - - if (syncAnim.update) { - // merge with previous update message - syncAnim.update.payload.animation = deepmerge( - syncAnim.update.payload.animation, - update.payload.animation); - } else { - // just save it - syncAnim.update = update; - } - } -} +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import deepmerge from 'deepmerge'; +import { EventEmitter } from 'events'; + +import { Guid, log, UserLike } from '../../..'; +import { + Client, + Connection, + EventedConnection, + InitializeActorMessage, + Message, + MissingRule, + Payloads, + Protocols, + Rules, + SessionExecution, + SessionHandshake, + SessionSync, + SyncActor, + SyncAnimation, + SyncAsset +} from '../../../internal'; + +type AssetCreationMessage = Message; +type AnimationCreationMessage = Message; + +/** + * @hidden + * Class for associating multiple client connections with a single app session. + */ +export class Session extends EventEmitter { + private _clientSet = new Map(); + private _actorSet = new Map>(); + private _assetSet = new Map>(); + private _assetCreatorSet = new Map(); + /** Maps animation IDs to animation sync structs */ + private _animationSet = new Map>(); + /** Maps IDs of messages that can create animations to the messages themselves */ + private _animationCreatorSet = new Map(); + private _userSet = new Map>(); + private _protocol: Protocols.Protocol; + private _disconnect: () => void; + + public get conn() { return this._conn; } + public get sessionId() { return this._sessionId; } + public get protocol() { return this._protocol; } + public get clients() { + return [...this._clientSet.values()].sort((a, b) => a.order - b.order); + } + + public get actors() { return [...this._actorSet.values()]; } + public get assets() { return [...this._assetSet.values()]; } + public get assetCreators() { return [...this._assetCreatorSet.values()]; } + public get animationSet() { return this._animationSet; } + public get animations() { return this._animationSet.values(); } + public get animationCreators() { return this._animationCreatorSet.values(); } + public get actorSet() { return this._actorSet; } + public get assetSet() { return this._assetSet; } + public get assetCreatorSet() { return this._assetCreatorSet; } + public get userSet() { return this._userSet; } + + public get rootActors() { + return this.actors.filter(a => !a.initialization.message.payload.actor.parentId); + } + public get authoritativeClient() { return this.clients.find(client => client.authoritative); } + public get peerAuthoritative() { return this._peerAuthoritative; } + + public client = (clientId: Guid) => this._clientSet.get(clientId); + public actor = (actorId: Guid) => this._actorSet.get(actorId); + public user = (userId: Guid) => this._userSet.get(userId); + public childrenOf = (parentId: Guid) => { + return this.actors.filter(actor => actor.initialization.message.payload.actor.parentId === parentId); + }; + public creatableChildrenOf = (parentId: Guid) => { + return this.actors.filter(actor => + actor.initialization.message.payload.actor.parentId === parentId + && !!actor.initialization.message.payload.type); + }; + + /** + * Creates a new Session instance + */ + constructor(private _conn: Connection, private _sessionId: string, private _peerAuthoritative: boolean) { + super(); + this.recvFromClient = this.recvFromClient.bind(this); + this.recvFromApp = this.recvFromApp.bind(this); + this._disconnect = this.disconnect.bind(this); + this._conn.on('close', this._disconnect); + this._conn.on('error', this._disconnect); + } + + /** + * Performs handshake and sync with the app + */ + public async connect() { + try { + const handshake = this._protocol = new SessionHandshake(this); + await handshake.run(); + + const sync = this._protocol = new SessionSync(this); + await sync.run(); + + const execution = this._protocol = new SessionExecution(this); + execution.on('recv', message => this.recvFromApp(message)); + execution.startListening(); + } catch (e) { + log.error('network', e); + this.disconnect(); + } + } + + public disconnect() { + try { + this._conn.off('close', this._disconnect); + this._conn.off('error', this._disconnect); + this._conn.close(); + this.emit('close'); + } catch { } + } + + /** + * Adds the client to the session + */ + public async join(client: Client) { + try { + this._clientSet.set(client.id, client); + client.on('close', () => this.leave(client.id)); + // Synchronize app state to the client. + await client.join(this); + // Once the client is joined, further messages from the client will be processed by the session + // (as opposed to a protocol class). + client.on('recv', (_: Client, message: Message) => this.recvFromClient(client, message)); + // If we don't have an authoritative client, make this client authoritative. + if (!this.authoritativeClient) { + this.setAuthoritativeClient(client.id); + } + } catch (e) { + log.error('network', e); + this.leave(client.id); + } + } + + /** + * Removes the client from the session + */ + public leave(clientId: Guid) { + try { + const client = this._clientSet.get(clientId); + this._clientSet.delete(clientId); + if (client) { + // If the client is associated with a userId, inform app the user is leaving + if (client.userId) { + this.protocol.sendPayload({ + type: 'user-left', + userId: client.userId + } as Payloads.UserLeft); + } + // Select another client to be the authoritative peer. + // TODO: Make selection criteria more intelligent (look at latency, prefer non-mobile, ...) + if (client.authoritative) { + const nextClient = this.clients.find(c => c.isJoined()); + if (nextClient) { + this.setAuthoritativeClient(nextClient.id); + } + } + } + // If this was the last client then shutdown the session + if (!this.clients.length) { + this._conn.close(); + } + } catch { } + } + + private setAuthoritativeClient(clientId: Guid) { + const newAuthority = this._clientSet.get(clientId); + if (!newAuthority) { + log.error('network', `[ERROR] setAuthoritativeClient: client ${clientId} does not exist.`); + return; + } + const oldAuthority = this.authoritativeClient; + + newAuthority.setAuthoritative(true); + for (const client of this.clients.filter(c => c !== newAuthority)) { + client.setAuthoritative(false); + } + + // forward connection quality metrics + if (this.conn instanceof EventedConnection) { + this.conn.linkConnectionQuality(newAuthority.conn.quality); + } + + // forward network stats from the authoritative peer connection to the app + const toApp = this.conn instanceof EventedConnection ? this.conn : null; + const forwardIncoming = (bytes: number) => toApp.statsTracker.recordIncoming(bytes); + const forwardOutgoing = (bytes: number) => toApp.statsTracker.recordOutgoing(bytes); + const toNewAuthority = newAuthority.conn instanceof EventedConnection ? newAuthority.conn : null; + if (toNewAuthority) { + toNewAuthority.statsTracker.on('incoming', forwardIncoming); + toNewAuthority.statsTracker.on('outgoing', forwardOutgoing); + } + + // turn off old authority + const toOldAuthority = oldAuthority && oldAuthority.conn instanceof EventedConnection + ? oldAuthority.conn : null; + if (toOldAuthority) { + toOldAuthority.statsTracker.off('incoming', forwardIncoming); + toOldAuthority.statsTracker.off('outgoing', forwardOutgoing); + } + } + + private recvFromClient = (client: Client, message: Message) => { + message = this.preprocessFromClient(client, message); + if (message) { + this.sendToApp(message); + } + }; + + private recvFromApp = (message: Message) => { + message = this.preprocessFromApp(message); + if (message) { + this.sendToClients(message); + } + }; + + public preprocessFromApp(message: Message): Message { + const rule = Rules[message.payload.type] || MissingRule; + const beforeReceiveFromApp = rule.session.beforeReceiveFromApp || (() => message); + return beforeReceiveFromApp(this, message); + } + + public preprocessFromClient(client: Client, message: Message): Message { + // Precaution: If we don't recognize this client, drop the message. + if (!this._clientSet.has(client.id)) { + return undefined; + } + if (message.payload && message.payload.type && message.payload.type.length) { + const rule = Rules[message.payload.type] || MissingRule; + const beforeReceiveFromClient = rule.session.beforeReceiveFromClient || (() => message); + message = beforeReceiveFromClient(this, client, message); + } + return message; + } + + public sendToApp(message: Message) { + this.protocol.sendMessage(message); + } + + public sendToClients(message: Message, filterFn?: (value: Client, index: number) => any) { + const clients = this.clients.filter(filterFn || (() => true)); + for (const client of clients) { + client.send({ ...message }); + } + } + + public sendPayloadToClients(payload: Partial, filterFn?: (value: Client, index: number) => any) { + this.sendToClients({ payload }, filterFn); + } + + /** @deprecated */ + public findAnimation(syncActor: Partial, animationName: string) { + return (syncActor.createdAnimations || []).find(item => item.message.payload.animationName === animationName); + } + + /** @deprecated */ + public isAnimating(syncActor: Partial): boolean { + if ((syncActor.createdAnimations || []).some(item => item.enabled)) { + return true; + } + if (syncActor.initialization && + syncActor.initialization.message && + syncActor.initialization.message.payload && + syncActor.initialization.message.payload.actor) { + const parent = this._actorSet.get(syncActor.initialization.message.payload.actor.parentId); + if (parent) { + return this.isAnimating(parent); + } + } + return false; + } + + public cacheInitializeActorMessage(message: InitializeActorMessage) { + let syncActor = this.actorSet.get(message.payload.actor.id); + if (!syncActor) { + const parent = this.actorSet.get(message.payload.actor.parentId); + syncActor = { + actorId: message.payload.actor.id, + exclusiveToUser: parent && parent.exclusiveToUser + || message.payload.actor.exclusiveToUser, + initialization: deepmerge({ message }, {}) + }; + this.actorSet.set(message.payload.actor.id, syncActor); + // update reserved actor init message with something the client can use + } else if (syncActor.initialization.message.payload.type === 'x-reserve-actor') { + // send real init message, but with session's initial actor state + message.payload = { + ...message.payload, + actor: syncActor.initialization.message.payload.actor + }; + // write the merged message back to the session + syncActor.initialization.message = message; + } + } + + public cacheActorUpdateMessage(message: Message) { + const syncActor = this.actorSet.get(message.payload.actor.id); + if (syncActor) { + // Merge the update into the existing actor. + syncActor.initialization.message.payload.actor + = deepmerge(syncActor.initialization.message.payload.actor, message.payload.actor); + + // strip out transform data that wasn't updated + // so it doesn't desync from the updated one + const cacheTransform = syncActor.initialization.message.payload.actor.transform; + const patchTransform = message.payload.actor.transform; + if (patchTransform && patchTransform.app && cacheTransform.local) { + delete cacheTransform.local.position; + delete cacheTransform.local.rotation; + } else if (patchTransform && patchTransform.local) { + delete cacheTransform.app; + } + } + } + + public cacheAssetCreationRequest(message: AssetCreationMessage) { + this.assetCreatorSet.set(message.id, message); + } + + public cacheAssetCreation(assetId: Guid, creatorId: Guid, duration?: number) { + const syncAsset = { + id: assetId, + creatorMessageId: creatorId, + duration + } as Partial; + this.assetSet.set(assetId, syncAsset); + const creator = this.assetCreatorSet.get(creatorId); + + // Updates are cached on send, creates are cached on receive, + // so it's possible something was updated while it was loading. + // Merge those updates into creation once the create comes back. + if (creator.payload.type === 'create-asset' && syncAsset.update) { + creator.payload.definition = deepmerge( + creator.payload.definition, + syncAsset.update.payload.asset + ); + syncAsset.update = undefined; + } + + // update end times on playing media instances with the now-known duration + for (const syncActor of this.actorSet.values()) { + for (const activeMediaInstance of (syncActor.activeMediaInstances || [])) { + if (activeMediaInstance.message.payload.mediaAssetId !== assetId || + activeMediaInstance.message.payload.options.looping === true || + activeMediaInstance.message.payload.options.paused === true || + duration === undefined) { + continue; + } + + let timeRemaining: number = syncAsset.duration; + if (activeMediaInstance.message.payload.options.time !== undefined) { + timeRemaining -= activeMediaInstance.message.payload.options.time; + } + if (activeMediaInstance.message.payload.options.pitch !== undefined) { + timeRemaining /= Math.pow(2.0, + (activeMediaInstance.message.payload.options.pitch / 12.0)); + } + activeMediaInstance.expirationTime = activeMediaInstance.basisTime + timeRemaining; + } + } + } + + public cacheAssetUpdate(update: Message) { + if (!this.assetSet.has(update.payload.asset.id)) { + this.assetSet.set(update.payload.asset.id, { id: update.payload.asset.id }); + } + const syncAsset = this.assetSet.get(update.payload.asset.id); + const creator = this.assetCreatorSet.get(syncAsset.creatorMessageId); + + if (creator && creator.payload.type === 'create-asset') { + // roll update into creation message + creator.payload.definition = deepmerge( + creator.payload.definition, + update.payload.asset); + } else if (syncAsset.update) { + // merge with previous update message + syncAsset.update.payload.asset = deepmerge( + syncAsset.update.payload.asset, + update.payload.asset); + } else { + // just save it + syncAsset.update = update; + } + } + + public cacheAssetUnload(containerId: Guid) { + const creators = this.assetCreators.filter(c => c.payload.containerId === containerId); + for (const creator of creators) { + // un-cache creation message + this.assetCreatorSet.delete(creator.id); + + // un-cache created assets + const assets = this.assets.filter(a => a.creatorMessageId === creator.id); + for (const asset of assets) { + this.assetSet.delete(asset.id); + } + } + } + + public cacheAnimationCreationRequest(payload: AnimationCreationMessage) { + this._animationCreatorSet.set(payload.id, payload); + } + + public cacheAnimationCreation(animId: Guid, creatorId: Guid, duration?: number) { + this._animationSet.set(animId, { + id: animId, + creatorMessageId: creatorId, + update: undefined, + duration + }); + } + + public cacheAnimationUpdate(update: Message) { + let syncAnim = this._animationSet.get(update.payload.animation.id); + if (!syncAnim) { + syncAnim = { id: update.payload.animation.id }; + this._animationSet.set(syncAnim.id, syncAnim); + } + + if (syncAnim.update) { + // merge with previous update message + syncAnim.update.payload.animation = deepmerge( + syncAnim.update.payload.animation, + update.payload.animation); + } else { + // just save it + syncAnim.update = update; + } + } +} diff --git a/packages/sdk/src/adapters/multipeer/syncActor.ts b/packages/sdk/src/internal/adapters/multipeer/syncActor.ts similarity index 84% rename from packages/sdk/src/adapters/multipeer/syncActor.ts rename to packages/sdk/src/internal/adapters/multipeer/syncActor.ts index 136268bd3..4c399cf2c 100644 --- a/packages/sdk/src/adapters/multipeer/syncActor.ts +++ b/packages/sdk/src/internal/adapters/multipeer/syncActor.ts @@ -1,48 +1,48 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -import { BehaviorType, Guid, Message } from '../..'; -import * as Payloads from '../../types/network/payloads'; - -/** @hidden */ -export type InitializeActorMessage = Message; - -/** - * @hidden - */ -export type InitializeActor = { - message: InitializeActorMessage; -}; - -/** - * @hidden - */ -export type CreateAnimation = { - message: Message; - enabled: boolean; -}; - -/** - * @hidden - */ -export type ActiveMediaInstance = { - message: Message; - basisTime: number; - expirationTime: number; -}; - -/** - * @hidden - */ -export type SyncActor = { - actorId: Guid; - initialization: InitializeActor; - createdAnimations: CreateAnimation[]; - activeMediaInstances: ActiveMediaInstance[]; - activeInterpolations: Payloads.InterpolateActor[]; - behavior: BehaviorType; - grabbedBy: Guid; - exclusiveToUser: Guid; -}; +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { BehaviorType, Guid } from '../../..'; +import { Message, Payloads } from '../../../internal'; + +/** @hidden */ +export type InitializeActorMessage = Message; + +/** + * @hidden + */ +export type InitializeActor = { + message: InitializeActorMessage; +}; + +/** + * @hidden + */ +export type CreateAnimation = { + message: Message; + enabled: boolean; +}; + +/** + * @hidden + */ +export type ActiveMediaInstance = { + message: Message; + basisTime: number; + expirationTime: number; +}; + +/** + * @hidden + */ +export type SyncActor = { + actorId: Guid; + initialization: InitializeActor; + createdAnimations: CreateAnimation[]; + activeMediaInstances: ActiveMediaInstance[]; + activeInterpolations: Payloads.InterpolateActor[]; + behavior: BehaviorType; + grabbedBy: Guid; + exclusiveToUser: Guid; +}; diff --git a/packages/sdk/src/adapters/multipeer/syncAnimation.ts b/packages/sdk/src/internal/adapters/multipeer/syncAnimation.ts similarity index 84% rename from packages/sdk/src/adapters/multipeer/syncAnimation.ts rename to packages/sdk/src/internal/adapters/multipeer/syncAnimation.ts index 9121a6bae..325eb03ba 100644 --- a/packages/sdk/src/adapters/multipeer/syncAnimation.ts +++ b/packages/sdk/src/internal/adapters/multipeer/syncAnimation.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. */ -import { Guid, Message } from '../..'; -import * as Payloads from '../../types/network/payloads'; +import { Guid } from '../../..'; +import { Message, Payloads } from '../../../internal'; /** @hidden */ export class SyncAnimation { diff --git a/packages/sdk/src/adapters/multipeer/syncAsset.ts b/packages/sdk/src/internal/adapters/multipeer/syncAsset.ts similarity index 83% rename from packages/sdk/src/adapters/multipeer/syncAsset.ts rename to packages/sdk/src/internal/adapters/multipeer/syncAsset.ts index 642d6b1ac..d9c254563 100644 --- a/packages/sdk/src/adapters/multipeer/syncAsset.ts +++ b/packages/sdk/src/internal/adapters/multipeer/syncAsset.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. */ -import { Guid, Message } from '../..'; -import * as Payloads from '../../types/network/payloads'; +import { Guid } from '../../..'; +import { Message, Payloads } from '../../../internal'; /** @hidden */ export class SyncAsset { diff --git a/packages/sdk/src/connection/connection.ts b/packages/sdk/src/internal/connection/connection.ts similarity index 87% rename from packages/sdk/src/connection/connection.ts rename to packages/sdk/src/internal/connection/connection.ts index e6021663f..13e922e75 100644 --- a/packages/sdk/src/connection/connection.ts +++ b/packages/sdk/src/internal/connection/connection.ts @@ -3,11 +3,14 @@ * Licensed under the MIT License. */ -import { Guid, Message } from '..'; -import { ExponentialMovingAverage } from '../utils/exponentialMovingAverage'; -import { QueuedPromise } from '../utils/queuedPromise'; -import { TrackingClock } from '../utils/trackingClock'; -import { NetworkStatsReport } from './networkStats'; +import { Guid } from '../..'; +import { + ExponentialMovingAverage, + Message, + NetworkStatsReport, + QueuedPromise, + TrackingClock +} from '../../internal'; /** * @hidden diff --git a/packages/sdk/src/connection/eventedConnection.ts b/packages/sdk/src/internal/connection/eventedConnection.ts similarity index 90% rename from packages/sdk/src/connection/eventedConnection.ts rename to packages/sdk/src/internal/connection/eventedConnection.ts index a2867d5db..a42e0eb9d 100644 --- a/packages/sdk/src/connection/eventedConnection.ts +++ b/packages/sdk/src/internal/connection/eventedConnection.ts @@ -4,10 +4,15 @@ */ import { EventEmitter } from 'events'; -import { Connection, ConnectionQuality } from '.'; -import { Guid, Message } from '..'; -import { QueuedPromise } from '../utils/queuedPromise'; -import { NetworkStatsReport, NetworkStatsTracker } from './networkStats'; +import { Guid } from '../..'; +import { + Connection, + ConnectionQuality, + Message, + NetworkStatsReport, + NetworkStatsTracker, + QueuedPromise +} from '../../internal'; /** * @hidden diff --git a/packages/sdk/src/connection/index.ts b/packages/sdk/src/internal/connection/index.ts similarity index 100% rename from packages/sdk/src/connection/index.ts rename to packages/sdk/src/internal/connection/index.ts index 2d3ef86d4..939f9ba1c 100644 --- a/packages/sdk/src/connection/index.ts +++ b/packages/sdk/src/internal/connection/index.ts @@ -4,8 +4,8 @@ */ export * from './connection'; -export * from './nullConnection'; export * from './eventedConnection'; -export * from './webSocket'; -export * from './pipe'; export * from './networkStats'; +export * from './nullConnection'; +export * from './pipe'; +export * from './webSocket'; diff --git a/packages/sdk/src/connection/networkStats.ts b/packages/sdk/src/internal/connection/networkStats.ts similarity index 100% rename from packages/sdk/src/connection/networkStats.ts rename to packages/sdk/src/internal/connection/networkStats.ts diff --git a/packages/sdk/src/connection/nullConnection.ts b/packages/sdk/src/internal/connection/nullConnection.ts similarity index 88% rename from packages/sdk/src/connection/nullConnection.ts rename to packages/sdk/src/internal/connection/nullConnection.ts index ae61cccde..ece6713b4 100644 --- a/packages/sdk/src/connection/nullConnection.ts +++ b/packages/sdk/src/internal/connection/nullConnection.ts @@ -4,9 +4,15 @@ */ import { EventEmitter } from 'events'; -import { Connection, ConnectionQuality, NetworkStatsReport } from '.'; -import { Guid, Message } from '..'; -import { QueuedPromise } from '../utils/queuedPromise'; + +import { Guid } from '../..'; +import { + Connection, + ConnectionQuality, + Message, + NetworkStatsReport, + QueuedPromise +} from '../../internal'; /** * @hidden diff --git a/packages/sdk/src/connection/pipe.ts b/packages/sdk/src/internal/connection/pipe.ts similarity index 95% rename from packages/sdk/src/connection/pipe.ts rename to packages/sdk/src/internal/connection/pipe.ts index f0c20afd5..1d303cd55 100644 --- a/packages/sdk/src/connection/pipe.ts +++ b/packages/sdk/src/internal/connection/pipe.ts @@ -3,8 +3,7 @@ * Licensed under the MIT License. */ -import { EventedConnection } from '.'; -import { Message } from '..'; +import { EventedConnection, Message } from '../../internal'; /** * @hidden diff --git a/packages/sdk/src/connection/webSocket.ts b/packages/sdk/src/internal/connection/webSocket.ts similarity index 88% rename from packages/sdk/src/connection/webSocket.ts rename to packages/sdk/src/internal/connection/webSocket.ts index 91ac6f4bf..01ac4e05d 100644 --- a/packages/sdk/src/connection/webSocket.ts +++ b/packages/sdk/src/internal/connection/webSocket.ts @@ -4,11 +4,9 @@ */ import * as WS from 'ws'; -import { EventedConnection } from '.'; -import { Message } from '..'; -import { log } from '../log'; -import filterEmpty from '../utils/filterEmpty'; -import validateJsonFieldName from '../utils/validateJsonFieldName'; + +import { log } from '../..'; +import { EventedConnection, filterEmpty, Message, validateJsonFieldName } from '../../internal'; /** * An implementation of the Connection interface that wraps a WebSocket. diff --git a/packages/sdk/src/constants.ts b/packages/sdk/src/internal/constants.ts similarity index 97% rename from packages/sdk/src/constants.ts rename to packages/sdk/src/internal/constants.ts index 06fa4586e..42091ca6f 100644 --- a/packages/sdk/src/constants.ts +++ b/packages/sdk/src/internal/constants.ts @@ -1,13 +1,13 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -/** @hidden */ -export const HTTPHeaders = { - SessionID: 'x-ms-mixed-reality-extension-sessionid', - PlatformID: 'x-ms-mixed-reality-extension-platformid', - LegacyProtocolVersion: 'x-ms-mixed-reality-extension-protocol-version', - CurrentClientVersion: 'x-ms-mixed-reality-extension-client-version', - MinimumSupportedSDKVersion: 'x-ms-mixed-reality-extension-min-sdk-version', -}; +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +/** @hidden */ +export const HTTPHeaders = { + SessionID: 'x-ms-mixed-reality-extension-sessionid', + PlatformID: 'x-ms-mixed-reality-extension-platformid', + LegacyProtocolVersion: 'x-ms-mixed-reality-extension-protocol-version', + CurrentClientVersion: 'x-ms-mixed-reality-extension-client-version', + MinimumSupportedSDKVersion: 'x-ms-mixed-reality-extension-min-sdk-version', +}; diff --git a/packages/sdk/src/utils/eventEmitterLike.ts b/packages/sdk/src/internal/eventEmitterLike.ts similarity index 97% rename from packages/sdk/src/utils/eventEmitterLike.ts rename to packages/sdk/src/internal/eventEmitterLike.ts index 01e07d2c4..866cb6437 100644 --- a/packages/sdk/src/utils/eventEmitterLike.ts +++ b/packages/sdk/src/internal/eventEmitterLike.ts @@ -1,12 +1,12 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -/** @hidden */ -export default interface EventEmitterLike { - on(event: string | symbol, listener: (...args: any[]) => void): this; - once(event: string | symbol, listener: (...args: any[]) => void): this; - off(event: string | symbol, listener: (...args: any[]) => void): this; - emit(event: string | symbol, ...args: any[]): boolean; -} +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +/** @hidden */ +export default interface EventEmitterLike { + on(event: string | symbol, listener: (...args: any[]) => void): this; + once(event: string | symbol, listener: (...args: any[]) => void): this; + off(event: string | symbol, listener: (...args: any[]) => void): this; + emit(event: string | symbol, ...args: any[]): boolean; +} diff --git a/packages/sdk/src/utils/exportedPromise.ts b/packages/sdk/src/internal/exportedPromise.ts similarity index 95% rename from packages/sdk/src/utils/exportedPromise.ts rename to packages/sdk/src/internal/exportedPromise.ts index a5efbf848..33ea93f9a 100644 --- a/packages/sdk/src/utils/exportedPromise.ts +++ b/packages/sdk/src/internal/exportedPromise.ts @@ -1,11 +1,11 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -/** @hidden */ -export interface ExportedPromise { - resolve: (...args: any[]) => void; - reject: (reason?: any) => void; - original?: Promise; -} +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +/** @hidden */ +export interface ExportedPromise { + resolve: (...args: any[]) => void; + reject: (reason?: any) => void; + original?: Promise; +} diff --git a/packages/sdk/src/internal/index.ts b/packages/sdk/src/internal/index.ts new file mode 100644 index 000000000..bf5e3dcbf --- /dev/null +++ b/packages/sdk/src/internal/index.ts @@ -0,0 +1,23 @@ +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +export * from './adapters'; +export * from './connection'; +import * as Payloads from './payloads'; +import * as Protocols from './protocols'; +export * from './util'; + +import * as Constants from './constants'; +export * from './eventEmitterLike'; +export * from './exportedPromise'; +export * from './message'; +export * from './operatingModel'; +export * from './operationResultCode'; +export * from './patchable'; +export * from './queuedPromise'; +export * from './subscriptionType'; +export * from './trace'; + +export { Constants, Payloads, Protocols }; diff --git a/packages/sdk/src/types/network/message.ts b/packages/sdk/src/internal/message.ts similarity index 82% rename from packages/sdk/src/types/network/message.ts rename to packages/sdk/src/internal/message.ts index d7cbe3b4f..3acd29742 100644 --- a/packages/sdk/src/types/network/message.ts +++ b/packages/sdk/src/internal/message.ts @@ -1,38 +1,38 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -import { Guid } from '../..'; -import { Payload } from './payloads'; - -/** - * @hidden - * A message sent over the network. - */ -export type Message = { - /** - * Unique id of this message. When sending, a new id will be assigned if not already so. - */ - id?: Guid; - - /** - * (Optional) If the client is replying to us, this is the id of the original message. - */ - replyToId?: Guid; - - /** - * (Server to client) The time the server sent this message. In milliseconds. - */ - serverTimeMs?: number; - - /** - * (Server to client) The estimated latency on the connection. In milliseconds. - */ - latencyEstimateMs?: number; - - /** - * The message payload. - */ - payload: Partial; -}; +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { Guid } from '..'; +import { Payloads } from '../internal'; + +/** + * @hidden + * A message sent over the network. + */ +export type Message = { + /** + * Unique id of this message. When sending, a new id will be assigned if not already so. + */ + id?: Guid; + + /** + * (Optional) If the client is replying to us, this is the id of the original message. + */ + replyToId?: Guid; + + /** + * (Server to client) The time the server sent this message. In milliseconds. + */ + serverTimeMs?: number; + + /** + * (Server to client) The estimated latency on the connection. In milliseconds. + */ + latencyEstimateMs?: number; + + /** + * The message payload. + */ + payload: Partial; +}; diff --git a/packages/sdk/src/types/network/operatingModel.ts b/packages/sdk/src/internal/operatingModel.ts similarity index 95% rename from packages/sdk/src/types/network/operatingModel.ts rename to packages/sdk/src/internal/operatingModel.ts index ff8687768..704ead85d 100644 --- a/packages/sdk/src/types/network/operatingModel.ts +++ b/packages/sdk/src/internal/operatingModel.ts @@ -1,12 +1,12 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -/** - * @hidden - */ -export enum OperatingModel { - ServerAuthoritative = 'server-authoritative', - PeerAuthoritative = 'peer-authoritative' -} +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +/** + * @hidden + */ +export enum OperatingModel { + ServerAuthoritative = 'server-authoritative', + PeerAuthoritative = 'peer-authoritative' +} diff --git a/packages/sdk/src/types/network/operationResultCode.ts b/packages/sdk/src/internal/operationResultCode.ts similarity index 95% rename from packages/sdk/src/types/network/operationResultCode.ts rename to packages/sdk/src/internal/operationResultCode.ts index 54b0bfcf4..00e684e49 100644 --- a/packages/sdk/src/types/network/operationResultCode.ts +++ b/packages/sdk/src/internal/operationResultCode.ts @@ -1,9 +1,9 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -/** - * @hidden - */ -export type OperationResultCode = 'success' | 'warning' | 'error'; +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +/** + * @hidden + */ +export type OperationResultCode = 'success' | 'warning' | 'error'; diff --git a/packages/sdk/src/types/patchable.ts b/packages/sdk/src/internal/patchable.ts similarity index 100% rename from packages/sdk/src/types/patchable.ts rename to packages/sdk/src/internal/patchable.ts diff --git a/packages/sdk/src/types/network/payloads/assets.ts b/packages/sdk/src/internal/payloads/assets.ts similarity index 93% rename from packages/sdk/src/types/network/payloads/assets.ts rename to packages/sdk/src/internal/payloads/assets.ts index 9125ab027..146baa522 100644 --- a/packages/sdk/src/types/network/payloads/assets.ts +++ b/packages/sdk/src/internal/payloads/assets.ts @@ -1,59 +1,59 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -import { CreateActorCommon, Payload } from '.'; -import { AssetLike, AssetSource, ColliderType, CollisionLayer, Guid } from '../../..'; - -export type CreateColliderType = ColliderType | 'none'; - -/** @hidden */ -export type AssetPayloadType - = 'assets-loaded' - | 'asset-update' - | 'create-asset' - | 'create-from-prefab' - | 'load-assets' - | 'unload-assets'; - -/** @hidden */ -export type LoadAssets = Payload & { - type: 'load-assets'; - containerId: Guid; - source: AssetSource; - colliderType: CreateColliderType; -}; - -/** @hidden */ -export type CreateAsset = Payload & { - type: 'create-asset'; - containerId: Guid; - definition: AssetLike; -}; - -/** @hidden */ -export type AssetsLoaded = Payload & { - type: 'assets-loaded'; - assets: AssetLike[]; - failureMessage: string; -}; - -/** @hidden */ -export type AssetUpdate = Payload & { - type: 'asset-update'; - asset: Partial; -}; - -/** @hidden */ -export type CreateFromPrefab = CreateActorCommon & { - type: 'create-from-prefab'; - prefabId: Guid; - collisionLayer?: CollisionLayer; -}; - -/** @hidden */ -export type UnloadAssets = Payload & { - type: 'unload-assets'; - containerId: Guid; -}; +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { CreateActorCommon, Payload } from '.'; +import { AssetLike, AssetSource, ColliderType, CollisionLayer, Guid } from '../..'; + +export type CreateColliderType = ColliderType | 'none'; + +/** @hidden */ +export type AssetPayloadType + = 'assets-loaded' + | 'asset-update' + | 'create-asset' + | 'create-from-prefab' + | 'load-assets' + | 'unload-assets'; + +/** @hidden */ +export type LoadAssets = Payload & { + type: 'load-assets'; + containerId: Guid; + source: AssetSource; + colliderType: CreateColliderType; +}; + +/** @hidden */ +export type CreateAsset = Payload & { + type: 'create-asset'; + containerId: Guid; + definition: AssetLike; +}; + +/** @hidden */ +export type AssetsLoaded = Payload & { + type: 'assets-loaded'; + assets: AssetLike[]; + failureMessage: string; +}; + +/** @hidden */ +export type AssetUpdate = Payload & { + type: 'asset-update'; + asset: Partial; +}; + +/** @hidden */ +export type CreateFromPrefab = CreateActorCommon & { + type: 'create-from-prefab'; + prefabId: Guid; + collisionLayer?: CollisionLayer; +}; + +/** @hidden */ +export type UnloadAssets = Payload & { + type: 'unload-assets'; + containerId: Guid; +}; diff --git a/packages/sdk/src/types/network/payloads/index.ts b/packages/sdk/src/internal/payloads/index.ts similarity index 95% rename from packages/sdk/src/types/network/payloads/index.ts rename to packages/sdk/src/internal/payloads/index.ts index 8795fda24..a89fa2266 100644 --- a/packages/sdk/src/types/network/payloads/index.ts +++ b/packages/sdk/src/internal/payloads/index.ts @@ -1,9 +1,9 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -export * from './payloads'; -export * from './assets'; -export * from './physics'; -export * from './sync'; +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +export * from './assets'; +export * from './payloads'; +export * from './physics'; +export * from './sync'; diff --git a/packages/sdk/src/types/network/payloads/payloads.ts b/packages/sdk/src/internal/payloads/payloads.ts similarity index 92% rename from packages/sdk/src/types/network/payloads/payloads.ts rename to packages/sdk/src/internal/payloads/payloads.ts index adfd2b894..00db84f06 100644 --- a/packages/sdk/src/types/network/payloads/payloads.ts +++ b/packages/sdk/src/internal/payloads/payloads.ts @@ -1,405 +1,407 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -import { OperationResultCode, Trace } from '..'; -import { - ActionState, - ActorLike, - AnimationLike, - BehaviorType, - CreateAnimationOptions, - Guid, - MediaCommand, - SetAnimationStateOptions, - SetMediaStateOptions, - TransformLike, - UserLike, -} from '../../..'; -import { OperatingModel } from '../operatingModel'; -import { AssetPayloadType } from './assets'; -import { SyncPayloadType } from './sync'; - -/** - * @hidden - * *** KEEP ENTRIES SORTED *** - */ -export type PayloadType - = AssetPayloadType - | SyncPayloadType - | 'actor-correction' - | 'actor-update' - | 'animation-update' - | 'app2engine-rpc' - | 'collision-event-raised' - | 'create-animation' - | 'create-empty' - | 'create-from-library' - | 'destroy-actors' - | 'dialog-response' - | 'engine2app-rpc' - | 'handshake' - | 'handshake-complete' - | 'handshake-reply' - | 'heartbeat' - | 'heartbeat-reply' - | 'interpolate-actor' - | 'multi-operation-result' - | 'object-spawned' - | 'operation-result' - | 'perform-action' - | 'rigidbody-add-force' - | 'rigidbody-add-force-at-position' - | 'rigidbody-add-relative-torque' - | 'rigidbody-add-torque' - | 'rigidbody-commands' - | 'rigidbody-move-position' - | 'rigidbody-move-rotation' - | 'set-animation-state' - | 'set-authoritative' - | 'set-behavior' - | 'set-media-state' - | 'show-dialog' - | 'sync-animations' - | 'sync-complete' - | 'sync-request' - | 'traces' - | 'trigger-event-raised' - | 'user-joined' - | 'user-left' - | 'user-update' - ; - -/** - * @hidden - * Base interface for Payloads. - */ -export type Payload = { - type: PayloadType; - traces?: Trace[]; -}; - -/** - * @hidden - * Engine to app. Contains a set of trace messages. - */ -export type Traces = Payload & { - type: 'traces'; -}; - -/** - * @hidden - * Engine to app. The result of an operation. - */ -export type OperationResult = Payload & { - type: 'operation-result'; - resultCode: OperationResultCode; - message: string; -}; - -/** - * @hidden - * Engine to app. The result of multiple operations. - */ -export type MultiOperationResult = Payload & { - type: 'multi-operation-result'; - results: OperationResult[]; -}; - -/** - * @hidden - * Engine to app. Handshake initiation. - */ -export type Handshake = Payload & { - type: 'handshake'; -}; - -/** - * @hidden - * App to engine. Response to Handshake. - */ -export type HandshakeReply = Payload & { - type: 'handshake-reply'; - sessionId: string; - operatingModel: OperatingModel; -}; - -/** - * @hidden - * Engine to app. Handshake process is complete. - */ -export type HandshakeComplete = Payload & { - type: 'handshake-complete'; -}; - -/** - * @hidden - */ -export type Heartbeat = Payload & { - type: 'heartbeat'; - serverTime: number; -}; - -/** - * @hidden - */ -export type HeartbeatReply = Payload & { - type: 'heartbeat-reply'; -}; - -/** - * @hidden - */ -export type AppToEngineRPC = Payload & { - type: 'app2engine-rpc'; - channelName?: string; - userId?: Guid; - procName: string; - args: any[]; -}; - -/** - * @hidden - */ -export type EngineToAppRPC = Payload & { - type: 'engine2app-rpc'; - channelName?: string; - userId?: Guid; - procName: string; - args: any[]; -}; - -/** - * @hidden - */ -export type CreateActorCommon = Payload & { - actor: Partial; -}; - -/** - * @hidden - * App to engine. Request for engine to load a game object from the host library. - * Response is an ObjectSpawned payload. - */ -export type CreateFromLibrary = CreateActorCommon & { - type: 'create-from-library'; - resourceId: string; -}; - -/** - * @hidden - * App to engine. Create an empty game object. - * Response is an ObjectSpawned payload. - */ -export type CreateEmpty = CreateActorCommon & { - type: 'create-empty'; -}; - -/** - * @hidden - * Engine to app. Response from LoadFromAssetBundle (and similar). - */ -export type ObjectSpawned = Payload & { - type: 'object-spawned'; - actors: Array>; - animations: Array>; - result: OperationResult; -}; - -/** - * @hidden - * Bi-directional. Changed properties of an actor object (sparsely populated). - */ -export type ActorUpdate = Payload & { - type: 'actor-update'; - actor: Partial; -}; - -/** - * @hidden - * Bi-directional. Change properties of an animation object (sparsely populated). - */ -export type AnimationUpdate = Payload & { - type: 'animation-update'; - animation: Partial; -}; - -/** - * @hidden - * Engine to app. Actor correction that should be lerped on the other clients. - */ -export type ActorCorrection = Payload & { - type: 'actor-correction'; - actorId: Guid; - appTransform: TransformLike; -}; - -/** - * @hidden - * Bi-directional. Command to destroy an actor (and its children). - */ -export type DestroyActors = Payload & { - type: 'destroy-actors'; - actorIds: Guid[]; -}; - -/** - * @hidden - * Engine to app. Engine wants all the application state. - */ -export type SyncRequest = Payload & { - type: 'sync-request'; -}; - -/** - * @hidden - * App to engine. Done sending engine the application state. - */ -export type SyncComplete = Payload & { - type: 'sync-complete'; -}; - -/** - * @hidden - * App to engine. Specific to multi-peer adapter. Set whether the client is "authoritative". The authoritative client - * sends additional updates back to the app such as rigid body updates and animation events. - */ -export type SetAuthoritative = Payload & { - type: 'set-authoritative'; - authoritative: boolean; -}; - -/** - * @hidden - * App to engine. The user has joined the app. - */ -export type UserJoined = Payload & { - type: 'user-joined'; - user: Partial; -}; - -/** - * @hidden - * Engine to app. The user has left the app. - */ -export type UserLeft = Payload & { - type: 'user-left'; - userId: Guid; -}; - -/** - * @hidden - * Engine to app. Update to the user's state. - * Only received for users who have joined the app. - */ -export type UserUpdate = Payload & { - type: 'user-update'; - user: Partial; -}; - -/** - * @hidden - * Engine to app. The user is performing an action for a behavior. - */ -export type PerformAction = Payload & { - type: 'perform-action'; - userId: Guid; - targetId: Guid; - behaviorType: BehaviorType; - actionName: string; - actionState: ActionState; -}; - -/** - * @hidden - * App to engine. Set the behavior on the actor with - * the given actor id. - */ -export type SetBehavior = Payload & { - type: 'set-behavior'; - actorId: Guid; - behaviorType: BehaviorType; -}; - -/** - * @hidden - * App to engine. Create an animation and associate it with an actor. - */ -export type CreateAnimation = Payload & CreateAnimationOptions & { - type: 'create-animation'; - actorId: Guid; - animationId?: string; - animationName: string; -}; - -/** - * @hidden - * @deprecated - * App to engine. Sets animation state. - */ -export type SetAnimationState = Payload & { - type: 'set-animation-state'; - actorId: Guid; - animationName: string; - state: SetAnimationStateOptions; -}; - -/** - * @hidden - * @deprecated - * Bidirectional. Sync animation state. - */ -export type SyncAnimations = Payload & { - type: 'sync-animations'; - animationStates: SetAnimationState[]; -}; - -/** - * @hidden - * App to engine. Starts playing a sound. - */ -export type SetMediaState = Payload & { - type: 'set-media-state'; - id: Guid; - actorId: Guid; - mediaAssetId: Guid; - mediaCommand: MediaCommand; - options: SetMediaStateOptions; -}; - -/** - * @hidden - * App to engine. Interpolate the actor's transform. - */ -export type InterpolateActor = Payload & { - type: 'interpolate-actor'; - actorId: Guid; - animationName: string; - value: Partial; - duration: number; - curve: number[]; - enabled: boolean; -}; - -/** - * @hidden - * App to engine. Prompt to show modal dialog box. - */ -export type ShowDialog = Payload & { - type: 'show-dialog'; - userId: Guid; - text: string; - acceptInput?: boolean; -}; - -/** - * @hidden - * Engine to app. Acknowledgement of modal dialog. - */ -export type DialogResponse = Payload & { - type: 'dialog-response'; - failureMessage: string; - submitted: boolean; - text?: string; -}; +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { + ActionState, + ActorLike, + AnimationLike, + BehaviorType, + CreateAnimationOptions, + Guid, + MediaCommand, + SetAnimationStateOptions, + SetMediaStateOptions, + TransformLike, + UserLike, +} from '../..'; +import { + OperatingModel, + OperationResultCode, + Payloads, + Trace +} from '../../internal'; + +/** + * @hidden + * *** KEEP ENTRIES SORTED *** + */ +export type PayloadType + = Payloads.AssetPayloadType + | Payloads.SyncPayloadType + | 'actor-correction' + | 'actor-update' + | 'animation-update' + | 'app2engine-rpc' + | 'collision-event-raised' + | 'create-animation' + | 'create-empty' + | 'create-from-library' + | 'destroy-actors' + | 'dialog-response' + | 'engine2app-rpc' + | 'handshake' + | 'handshake-complete' + | 'handshake-reply' + | 'heartbeat' + | 'heartbeat-reply' + | 'interpolate-actor' + | 'multi-operation-result' + | 'object-spawned' + | 'operation-result' + | 'perform-action' + | 'rigidbody-add-force' + | 'rigidbody-add-force-at-position' + | 'rigidbody-add-relative-torque' + | 'rigidbody-add-torque' + | 'rigidbody-commands' + | 'rigidbody-move-position' + | 'rigidbody-move-rotation' + | 'set-animation-state' + | 'set-authoritative' + | 'set-behavior' + | 'set-media-state' + | 'show-dialog' + | 'sync-animations' + | 'sync-complete' + | 'sync-request' + | 'traces' + | 'trigger-event-raised' + | 'user-joined' + | 'user-left' + | 'user-update' + ; + +/** + * @hidden + * Base interface for Payloads. + */ +export type Payload = { + type: PayloadType; + traces?: Trace[]; +}; + +/** + * @hidden + * Engine to app. Contains a set of trace messages. + */ +export type Traces = Payload & { + type: 'traces'; +}; + +/** + * @hidden + * Engine to app. The result of an operation. + */ +export type OperationResult = Payload & { + type: 'operation-result'; + resultCode: OperationResultCode; + message: string; +}; + +/** + * @hidden + * Engine to app. The result of multiple operations. + */ +export type MultiOperationResult = Payload & { + type: 'multi-operation-result'; + results: OperationResult[]; +}; + +/** + * @hidden + * Engine to app. Handshake initiation. + */ +export type Handshake = Payload & { + type: 'handshake'; +}; + +/** + * @hidden + * App to engine. Response to Handshake. + */ +export type HandshakeReply = Payload & { + type: 'handshake-reply'; + sessionId: string; + operatingModel: OperatingModel; +}; + +/** + * @hidden + * Engine to app. Handshake process is complete. + */ +export type HandshakeComplete = Payload & { + type: 'handshake-complete'; +}; + +/** + * @hidden + */ +export type Heartbeat = Payload & { + type: 'heartbeat'; + serverTime: number; +}; + +/** + * @hidden + */ +export type HeartbeatReply = Payload & { + type: 'heartbeat-reply'; +}; + +/** + * @hidden + */ +export type AppToEngineRPC = Payload & { + type: 'app2engine-rpc'; + channelName?: string; + userId?: Guid; + procName: string; + args: any[]; +}; + +/** + * @hidden + */ +export type EngineToAppRPC = Payload & { + type: 'engine2app-rpc'; + channelName?: string; + userId?: Guid; + procName: string; + args: any[]; +}; + +/** + * @hidden + */ +export type CreateActorCommon = Payload & { + actor: Partial; +}; + +/** + * @hidden + * App to engine. Request for engine to load a game object from the host library. + * Response is an ObjectSpawned payload. + */ +export type CreateFromLibrary = CreateActorCommon & { + type: 'create-from-library'; + resourceId: string; +}; + +/** + * @hidden + * App to engine. Create an empty game object. + * Response is an ObjectSpawned payload. + */ +export type CreateEmpty = CreateActorCommon & { + type: 'create-empty'; +}; + +/** + * @hidden + * Engine to app. Response from LoadFromAssetBundle (and similar). + */ +export type ObjectSpawned = Payload & { + type: 'object-spawned'; + actors: Array>; + animations: Array>; + result: OperationResult; +}; + +/** + * @hidden + * Bi-directional. Changed properties of an actor object (sparsely populated). + */ +export type ActorUpdate = Payload & { + type: 'actor-update'; + actor: Partial; +}; + +/** + * @hidden + * Bi-directional. Change properties of an animation object (sparsely populated). + */ +export type AnimationUpdate = Payload & { + type: 'animation-update'; + animation: Partial; +}; + +/** + * @hidden + * Engine to app. Actor correction that should be lerped on the other clients. + */ +export type ActorCorrection = Payload & { + type: 'actor-correction'; + actorId: Guid; + appTransform: TransformLike; +}; + +/** + * @hidden + * Bi-directional. Command to destroy an actor (and its children). + */ +export type DestroyActors = Payload & { + type: 'destroy-actors'; + actorIds: Guid[]; +}; + +/** + * @hidden + * Engine to app. Engine wants all the application state. + */ +export type SyncRequest = Payload & { + type: 'sync-request'; +}; + +/** + * @hidden + * App to engine. Done sending engine the application state. + */ +export type SyncComplete = Payload & { + type: 'sync-complete'; +}; + +/** + * @hidden + * App to engine. Specific to multi-peer adapter. Set whether the client is "authoritative". The authoritative client + * sends additional updates back to the app such as rigid body updates and animation events. + */ +export type SetAuthoritative = Payload & { + type: 'set-authoritative'; + authoritative: boolean; +}; + +/** + * @hidden + * App to engine. The user has joined the app. + */ +export type UserJoined = Payload & { + type: 'user-joined'; + user: Partial; +}; + +/** + * @hidden + * Engine to app. The user has left the app. + */ +export type UserLeft = Payload & { + type: 'user-left'; + userId: Guid; +}; + +/** + * @hidden + * Engine to app. Update to the user's state. + * Only received for users who have joined the app. + */ +export type UserUpdate = Payload & { + type: 'user-update'; + user: Partial; +}; + +/** + * @hidden + * Engine to app. The user is performing an action for a behavior. + */ +export type PerformAction = Payload & { + type: 'perform-action'; + userId: Guid; + targetId: Guid; + behaviorType: BehaviorType; + actionName: string; + actionState: ActionState; +}; + +/** + * @hidden + * App to engine. Set the behavior on the actor with + * the given actor id. + */ +export type SetBehavior = Payload & { + type: 'set-behavior'; + actorId: Guid; + behaviorType: BehaviorType; +}; + +/** + * @hidden + * App to engine. Create an animation and associate it with an actor. + */ +export type CreateAnimation = Payload & CreateAnimationOptions & { + type: 'create-animation'; + actorId: Guid; + animationId?: string; + animationName: string; +}; + +/** + * @hidden + * @deprecated + * App to engine. Sets animation state. + */ +export type SetAnimationState = Payload & { + type: 'set-animation-state'; + actorId: Guid; + animationName: string; + state: SetAnimationStateOptions; +}; + +/** + * @hidden + * @deprecated + * Bidirectional. Sync animation state. + */ +export type SyncAnimations = Payload & { + type: 'sync-animations'; + animationStates: SetAnimationState[]; +}; + +/** + * @hidden + * App to engine. Starts playing a sound. + */ +export type SetMediaState = Payload & { + type: 'set-media-state'; + id: Guid; + actorId: Guid; + mediaAssetId: Guid; + mediaCommand: MediaCommand; + options: SetMediaStateOptions; +}; + +/** + * @hidden + * App to engine. Interpolate the actor's transform. + */ +export type InterpolateActor = Payload & { + type: 'interpolate-actor'; + actorId: Guid; + animationName: string; + value: Partial; + duration: number; + curve: number[]; + enabled: boolean; +}; + +/** + * @hidden + * App to engine. Prompt to show modal dialog box. + */ +export type ShowDialog = Payload & { + type: 'show-dialog'; + userId: Guid; + text: string; + acceptInput?: boolean; +}; + +/** + * @hidden + * Engine to app. Acknowledgement of modal dialog. + */ +export type DialogResponse = Payload & { + type: 'dialog-response'; + failureMessage: string; + submitted: boolean; + text?: string; +}; diff --git a/packages/sdk/src/types/network/payloads/physics.ts b/packages/sdk/src/internal/payloads/physics.ts similarity index 72% rename from packages/sdk/src/types/network/payloads/physics.ts rename to packages/sdk/src/internal/payloads/physics.ts index 7db3ad812..2efcee357 100644 --- a/packages/sdk/src/types/network/payloads/physics.ts +++ b/packages/sdk/src/internal/payloads/physics.ts @@ -1,101 +1,101 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -import { Payload } from '.'; -import { - CollisionData, - CollisionEventType, - Guid, - TriggerEventType, - QuaternionLike, - Vector3Like -} from '../../..'; - -/** - * @hidden - * App to engine. Send a rigidbody command - */ -export type RigidBodyCommands = Payload & { - type: 'rigidbody-commands'; - actorId: Guid; - commandPayloads: Payload[]; -}; - -/** - * @hidden - * App to engine. Move position of rigidbody - */ -export type RigidBodyMovePosition = Payload & { - type: 'rigidbody-move-position'; - position: Partial; -}; - -/** - * @hidden - * App to engine. Move rotation of rigidbody - */ -export type RigidBodyMoveRotation = Payload & { - type: 'rigidbody-move-rotation'; - rotation: QuaternionLike; -}; - -/** - * @hidden - * App to engine. Add force rigidbody command - */ -export type RigidBodyAddForce = Payload & { - type: 'rigidbody-add-force'; - force: Partial; -}; - -/** - * @hidden - * App to engine. Add force at position rigidbody command - */ -export type RigidBodyAddForceAtPosition = Payload & { - type: 'rigidbody-add-force-at-position'; - force: Partial; - position: Partial; -}; - -/** - * @hidden - * App to engine. Add force rigidbody command - */ -export type RigidBodyAddTorque = Payload & { - type: 'rigidbody-add-torque'; - torque: Partial; -}; - -/** - * @hidden - * App to engine. Add force at position rigidbody command - */ -export type RigidBodyAddRelativeTorque = Payload & { - type: 'rigidbody-add-relative-torque'; - relativeTorque: Partial; -}; - -/** - * @hidden - * Engine to app. Collision event data from engine after a collision has occured. - */ -export type CollisionEventRaised = Payload & { - type: 'collision-event-raised'; - actorId: Guid; - eventType: CollisionEventType; - collisionData: CollisionData; -}; - -/** - * @hidden - * Engine to app. Trigger event data from engine after a trigger event has occured. - */ -export type TriggerEventRaised = Payload & { - type: 'trigger-event-rasied'; - actorId: Guid; - eventType: TriggerEventType; - otherActorId: Guid; -}; +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { Payloads } from '../../internal'; +import { + CollisionData, + CollisionEventType, + Guid, + TriggerEventType, + QuaternionLike, + Vector3Like +} from '../..'; + +/** + * @hidden + * App to engine. Send a rigidbody command + */ +export type RigidBodyCommands = Payloads.Payload & { + type: 'rigidbody-commands'; + actorId: Guid; + commandPayloads: Payloads.Payload[]; +}; + +/** + * @hidden + * App to engine. Move position of rigidbody + */ +export type RigidBodyMovePosition = Payloads.Payload & { + type: 'rigidbody-move-position'; + position: Partial; +}; + +/** + * @hidden + * App to engine. Move rotation of rigidbody + */ +export type RigidBodyMoveRotation = Payloads.Payload & { + type: 'rigidbody-move-rotation'; + rotation: QuaternionLike; +}; + +/** + * @hidden + * App to engine. Add force rigidbody command + */ +export type RigidBodyAddForce = Payloads.Payload & { + type: 'rigidbody-add-force'; + force: Partial; +}; + +/** + * @hidden + * App to engine. Add force at position rigidbody command + */ +export type RigidBodyAddForceAtPosition = Payloads.Payload & { + type: 'rigidbody-add-force-at-position'; + force: Partial; + position: Partial; +}; + +/** + * @hidden + * App to engine. Add force rigidbody command + */ +export type RigidBodyAddTorque = Payloads.Payload & { + type: 'rigidbody-add-torque'; + torque: Partial; +}; + +/** + * @hidden + * App to engine. Add force at position rigidbody command + */ +export type RigidBodyAddRelativeTorque = Payloads.Payload & { + type: 'rigidbody-add-relative-torque'; + relativeTorque: Partial; +}; + +/** + * @hidden + * Engine to app. Collision event data from engine after a collision has occured. + */ +export type CollisionEventRaised = Payloads.Payload & { + type: 'collision-event-raised'; + actorId: Guid; + eventType: CollisionEventType; + collisionData: CollisionData; +}; + +/** + * @hidden + * Engine to app. Trigger event data from engine after a trigger event has occured. + */ +export type TriggerEventRaised = Payloads.Payload & { + type: 'trigger-event-rasied'; + actorId: Guid; + eventType: TriggerEventType; + otherActorId: Guid; +}; diff --git a/packages/sdk/src/types/network/payloads/sync.ts b/packages/sdk/src/internal/payloads/sync.ts similarity index 74% rename from packages/sdk/src/types/network/payloads/sync.ts rename to packages/sdk/src/internal/payloads/sync.ts index 71cfd8cea..c3c7078b3 100644 --- a/packages/sdk/src/types/network/payloads/sync.ts +++ b/packages/sdk/src/internal/payloads/sync.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. */ -import { CreateActorCommon } from '.'; +import { Payloads } from '../../internal'; /** @hidden */ export type SyncPayloadType = 'x-reserve-actor'; @@ -12,6 +12,6 @@ export type SyncPayloadType = 'x-reserve-actor'; * @hidden * Send a message to the multipeer adapter to save the given actor in the session */ -export type XReserveActor = CreateActorCommon & { +export type XReserveActor = Payloads.CreateActorCommon & { type: 'x-reserve-actor'; }; diff --git a/packages/sdk/src/protocols/clientPreprocessing.ts b/packages/sdk/src/internal/protocols/clientPreprocessing.ts similarity index 70% rename from packages/sdk/src/protocols/clientPreprocessing.ts rename to packages/sdk/src/internal/protocols/clientPreprocessing.ts index 2d104cff3..653ef5b0b 100644 --- a/packages/sdk/src/protocols/clientPreprocessing.ts +++ b/packages/sdk/src/internal/protocols/clientPreprocessing.ts @@ -1,37 +1,35 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -import { Middleware, Protocol } from '.'; -import { Message } from '..'; -import { HeartbeatReply } from '../types/network/payloads'; - -/** - * @hidden - */ -export class ClientPreprocessing implements Middleware { - constructor(private protocol: Protocol) { - this.beforeRecv = this.beforeRecv.bind(this); - } - - /** @private */ - public beforeRecv = (message: Message): Message => { - if (message.serverTimeMs > 0) { - this.protocol.conn.quality.trackingClock.update(message.serverTimeMs); - } - if (message.latencyEstimateMs > 0) { - this.protocol.conn.quality.latencyMs.update(message.latencyEstimateMs); - } - if (message.payload.type === 'heartbeat') { - this.protocol.sendMessage({ - replyToId: message.id, - payload: { - type: 'heartbeat-reply', - } as HeartbeatReply, - }); - message = undefined; - } - return message; - }; -} +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { Message, Payloads, Protocols } from '../../internal'; + +/** + * @hidden + */ +export class ClientPreprocessing implements Protocols.Middleware { + constructor(private protocol: Protocols.Protocol) { + this.beforeRecv = this.beforeRecv.bind(this); + } + + /** @private */ + public beforeRecv = (message: Message): Message => { + if (message.serverTimeMs > 0) { + this.protocol.conn.quality.trackingClock.update(message.serverTimeMs); + } + if (message.latencyEstimateMs > 0) { + this.protocol.conn.quality.latencyMs.update(message.latencyEstimateMs); + } + if (message.payload.type === 'heartbeat') { + this.protocol.sendMessage({ + replyToId: message.id, + payload: { + type: 'heartbeat-reply', + } as Payloads.HeartbeatReply, + }); + message = undefined; + } + return message; + }; +} diff --git a/packages/sdk/src/protocols/execution.ts b/packages/sdk/src/internal/protocols/execution.ts similarity index 61% rename from packages/sdk/src/protocols/execution.ts rename to packages/sdk/src/internal/protocols/execution.ts index 2cf2d897f..fe7946660 100644 --- a/packages/sdk/src/protocols/execution.ts +++ b/packages/sdk/src/internal/protocols/execution.ts @@ -1,158 +1,139 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -import { Protocol, ServerPreprocessing } from '.'; -import { ActionEvent, CollisionEvent, Context, Message, TriggerEvent, WebSocket } from '..'; -import { - ActorUpdate, - CollisionEventRaised, - DestroyActors, - EngineToAppRPC, - MultiOperationResult, - ObjectSpawned, - OperationResult, - PerformAction, - SetAnimationState, - SyncRequest, - Traces, - TriggerEventRaised, - UserJoined, - UserLeft, - UserUpdate, -} from '../types/network/payloads'; -import { log } from './../log'; -import { Sync } from './sync'; - -/** - * @hidden - * Class to handle operational messages with a client. - */ -export class Execution extends Protocol { - constructor(private context: Context) { - super(context.conn); - // Behave like a server-side endpoint (send heartbeats, measure connection quality) - this.use(new ServerPreprocessing()); - } - - /** @override */ - protected missingPromiseForReplyMessage(message: Message) { - // Ignore. App receives reply messages from all clients, but only processes the first one. - } - - /** @private */ - public 'recv-engine2app-rpc' = (payload: EngineToAppRPC) => { - this.emit('protocol.receive-rpc', payload); - }; - - /** @private */ - public 'recv-object-spawned' = (payload: ObjectSpawned) => { - this.emit('protocol.update-actors', payload.actors); - this.emit('protocol.update-animations', payload.animations); - }; - - /** @private */ - public 'recv-actor-update' = (payload: ActorUpdate) => { - this.emit('protocol.update-actors', [payload.actor]); - }; - - /** @private */ - public 'recv-destroy-actors' = (payload: DestroyActors) => { - this.emit('protocol.destroy-actors', payload.actorIds); - }; - - /** @private */ - public 'recv-operation-result' = (operationResult: OperationResult) => { - log.log('network', operationResult.resultCode, operationResult.message); - if (Array.isArray(operationResult.traces)) { - operationResult.traces.forEach(trace => { - log.log('network', trace.severity, trace.message); - }); - } - }; - - /** @private */ - public 'recv-multi-operation-result' = (multiOperationResult: MultiOperationResult) => { - throw new Error("Not implemented"); - }; - - /** @private */ - public 'recv-traces' = (payload: Traces) => { - payload.traces.forEach(trace => { - log.log('client', trace.severity, trace.message); - }); - }; - - /** @private */ - public 'recv-user-joined' = (payload: UserJoined) => { - - const props = payload.user.properties = payload.user.properties || {}; - props.host = props.host || 'unspecified'; - props.engine = props.engine || 'unspecified'; - - if (this.conn instanceof WebSocket && !props.remoteAddress) { - props.remoteAddress = this.conn.remoteAddress; - } - - this.emit('protocol.user-joined', payload.user); - }; - - /** @private */ - public 'recv-user-left' = (payload: UserLeft) => { - this.emit('protocol.user-left', payload.userId); - }; - - /** @private */ - public 'recv-user-update' = (payload: UserUpdate) => { - this.emit('protocol.update-user', payload.user); - }; - - /** @private */ - public 'recv-sync-request' = async (payload: SyncRequest) => { - // Switch over to the Sync protocol to handle this request - this.stopListening(); - - const sync = new Sync(this.conn); - await sync.run(); // Allow exception to propagate. - - this.startListening(); - }; - - /** @private */ - public 'recv-perform-action' = (payload: PerformAction) => { - this.emit('protocol.perform-action', { - user: this.context.user(payload.userId), - targetId: payload.targetId, - behaviorType: payload.behaviorType, - actionName: payload.actionName, - actionState: payload.actionState - } as ActionEvent); - }; - - /** @private */ - public 'recv-collision-event-raised' = (payload: CollisionEventRaised) => { - this.emit('protocol.collision-event-raised', { - colliderOwnerId: payload.actorId, - eventType: payload.eventType, - collisionData: payload.collisionData - } as CollisionEvent); - }; - - /** @private */ - public 'recv-trigger-event-raised' = (payload: TriggerEventRaised) => { - this.emit('protocol.trigger-event-raised', { - colliderOwnerId: payload.actorId, - eventType: payload.eventType, - otherColliderOwnerId: payload.otherActorId - } as TriggerEvent); - }; - - /** @private */ - public 'recv-set-animation-state' = (payload: SetAnimationState) => { - this.emit('protocol.set-animation-state', - payload.actorId, - payload.animationName, - payload.state); - }; -} +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { ActionEvent, CollisionEvent, Context, log, TriggerEvent, } from '../..'; +import { Message, Payloads, Protocols, WebSocket } from '../../internal'; + +/** + * @hidden + * Class to handle operational messages with a client. + */ +export class Execution extends Protocols.Protocol { + constructor(private context: Context) { + super(context.conn); + // Behave like a server-side endpoint (send heartbeats, measure connection quality) + this.use(new Protocols.ServerPreprocessing()); + } + + /** @override */ + protected missingPromiseForReplyMessage(message: Message) { + // Ignore. App receives reply messages from all clients, but only processes the first one. + } + + /** @private */ + public 'recv-engine2app-rpc' = (payload: Payloads.EngineToAppRPC) => { + this.emit('protocol.receive-rpc', payload); + }; + + /** @private */ + public 'recv-object-spawned' = (payload: Payloads.ObjectSpawned) => { + this.emit('protocol.update-actors', payload.actors); + this.emit('protocol.update-animations', payload.animations); + }; + + /** @private */ + public 'recv-actor-update' = (payload: Payloads.ActorUpdate) => { + this.emit('protocol.update-actors', [payload.actor]); + }; + + /** @private */ + public 'recv-destroy-actors' = (payload: Payloads.DestroyActors) => { + this.emit('protocol.destroy-actors', payload.actorIds); + }; + + /** @private */ + public 'recv-operation-result' = (operationResult: Payloads.OperationResult) => { + log.log('network', operationResult.resultCode, operationResult.message); + if (Array.isArray(operationResult.traces)) { + operationResult.traces.forEach(trace => { + log.log('network', trace.severity, trace.message); + }); + } + }; + + /** @private */ + public 'recv-multi-operation-result' = (multiOperationResult: Payloads.MultiOperationResult) => { + throw new Error("Not implemented"); + }; + + /** @private */ + public 'recv-traces' = (payload: Payloads.Traces) => { + payload.traces.forEach(trace => { + log.log('client', trace.severity, trace.message); + }); + }; + + /** @private */ + public 'recv-user-joined' = (payload: Payloads.UserJoined) => { + + const props = payload.user.properties = payload.user.properties || {}; + props.host = props.host || 'unspecified'; + props.engine = props.engine || 'unspecified'; + + if (this.conn instanceof WebSocket && !props.remoteAddress) { + props.remoteAddress = this.conn.remoteAddress; + } + + this.emit('protocol.user-joined', payload.user); + }; + + /** @private */ + public 'recv-user-left' = (payload: Payloads.UserLeft) => { + this.emit('protocol.user-left', payload.userId); + }; + + /** @private */ + public 'recv-user-update' = (payload: Payloads.UserUpdate) => { + this.emit('protocol.update-user', payload.user); + }; + + /** @private */ + public 'recv-sync-request' = async (payload: Payloads.SyncRequest) => { + // Switch over to the Sync protocol to handle this request + this.stopListening(); + + const sync = new Protocols.Sync(this.conn); + await sync.run(); // Allow exception to propagate. + + this.startListening(); + }; + + /** @private */ + public 'recv-perform-action' = (payload: Payloads.PerformAction) => { + this.emit('protocol.perform-action', { + user: this.context.user(payload.userId), + targetId: payload.targetId, + behaviorType: payload.behaviorType, + actionName: payload.actionName, + actionState: payload.actionState + } as ActionEvent); + }; + + /** @private */ + public 'recv-collision-event-raised' = (payload: Payloads.CollisionEventRaised) => { + this.emit('protocol.collision-event-raised', { + colliderOwnerId: payload.actorId, + eventType: payload.eventType, + collisionData: payload.collisionData + } as CollisionEvent); + }; + + /** @private */ + public 'recv-trigger-event-raised' = (payload: Payloads.TriggerEventRaised) => { + this.emit('protocol.trigger-event-raised', { + colliderOwnerId: payload.actorId, + eventType: payload.eventType, + otherColliderOwnerId: payload.otherActorId + } as TriggerEvent); + }; + + /** @private */ + public 'recv-set-animation-state' = (payload: Payloads.SetAnimationState) => { + this.emit('protocol.set-animation-state', + payload.actorId, + payload.animationName, + payload.state); + }; +} diff --git a/packages/sdk/src/protocols/handshake.ts b/packages/sdk/src/internal/protocols/handshake.ts similarity index 75% rename from packages/sdk/src/protocols/handshake.ts rename to packages/sdk/src/internal/protocols/handshake.ts index d19b0e0f3..acfee75b6 100644 --- a/packages/sdk/src/protocols/handshake.ts +++ b/packages/sdk/src/internal/protocols/handshake.ts @@ -1,45 +1,41 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -import { ServerPreprocessing } from '.'; -import { Connection } from '..'; -import { OperatingModel } from '../types/network/operatingModel'; -import * as Payloads from '../types/network/payloads'; -import { Protocol } from './protocol'; - -/** - * @hidden - * Class to manage the handshake process with a client. - */ -export class Handshake extends Protocol { - public syncRequest: Payloads.SyncRequest; - - constructor(conn: Connection, private sessionId: string, private operatingModel: OperatingModel) { - super(conn); - // Behave like a server-side endpoint (send heartbeats, measure connection quality) - this.use(new ServerPreprocessing()); - } - - /** @private */ - public 'recv-handshake' = (payload: Payloads.Handshake) => { - this.sendPayload({ - type: 'handshake-reply', - sessionId: this.sessionId, - operatingModel: this.operatingModel, - } as Payloads.HandshakeReply); - }; - - /** @private */ - public 'recv-handshake-complete' = (payload: Payloads.HandshakeComplete) => { - this.resolve(); - }; - - /** @private */ - public 'recv-sync-request' = (payload: Payloads.SyncRequest) => { - // The way the protocol works right now, this message can be sent unexpectedly early by the client. - // If we receive it, we'll cache it and pass it along to the next protocol. - this.syncRequest = payload; - }; -} +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { Connection, OperatingModel, Payloads, Protocols } from '../../internal'; + +/** + * @hidden + * Class to manage the handshake process with a client. + */ +export class Handshake extends Protocols.Protocol { + public syncRequest: Payloads.SyncRequest; + + constructor(conn: Connection, private sessionId: string, private operatingModel: OperatingModel) { + super(conn); + // Behave like a server-side endpoint (send heartbeats, measure connection quality) + this.use(new Protocols.ServerPreprocessing()); + } + + /** @private */ + public 'recv-handshake' = (payload: Payloads.Handshake) => { + this.sendPayload({ + type: 'handshake-reply', + sessionId: this.sessionId, + operatingModel: this.operatingModel, + } as Payloads.HandshakeReply); + }; + + /** @private */ + public 'recv-handshake-complete' = (payload: Payloads.HandshakeComplete) => { + this.resolve(); + }; + + /** @private */ + public 'recv-sync-request' = (payload: Payloads.SyncRequest) => { + // The way the protocol works right now, this message can be sent unexpectedly early by the client. + // If we receive it, we'll cache it and pass it along to the next protocol. + this.syncRequest = payload; + }; +} diff --git a/packages/sdk/src/protocols/heartbeat.ts b/packages/sdk/src/internal/protocols/heartbeat.ts similarity index 85% rename from packages/sdk/src/protocols/heartbeat.ts rename to packages/sdk/src/internal/protocols/heartbeat.ts index 3a0632c7c..53534bc71 100644 --- a/packages/sdk/src/protocols/heartbeat.ts +++ b/packages/sdk/src/internal/protocols/heartbeat.ts @@ -1,49 +1,48 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -import { Protocol } from '.'; -import * as Payloads from '../types/network/payloads'; - -const MS_PER_S = 1e3; -const MS_PER_NS = 1e-6; -/** - * @hidden - * Periodically measures performance characteristics of the connection (latency). - */ -export class Heartbeat { - /** - * Creates a new Heartbeat instance. - * @param protocol The parent protocol object. - */ - constructor(private protocol: Protocol) { - } - - /** - * Polls connection quality the specified number of times. - */ - public async runIterations(sampleCount: number) { - for (let i = 0; i < sampleCount; ++i) { - await this.send(); // Allow exceptions to propagate out. - } - } - - public send() { - return new Promise((resolve, reject) => { - const start = process.hrtime(); - this.protocol.sendPayload({ - type: 'heartbeat', - serverTime: Date.now() - } as Payloads.Heartbeat, { - resolve: () => { - const hrInterval = process.hrtime(start); - const latency = hrInterval[0] * MS_PER_S + hrInterval[1] * MS_PER_NS; - this.protocol.conn.quality.latencyMs.update(latency); - resolve(latency); - }, - reject - }); - }); - } -} +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { Payloads, Protocols } from '../../internal'; + +const MS_PER_S = 1e3; +const MS_PER_NS = 1e-6; +/** + * @hidden + * Periodically measures performance characteristics of the connection (latency). + */ +export class Heartbeat { + /** + * Creates a new Heartbeat instance. + * @param protocol The parent protocol object. + */ + constructor(private protocol: Protocols.Protocol) { + } + + /** + * Polls connection quality the specified number of times. + */ + public async runIterations(sampleCount: number) { + for (let i = 0; i < sampleCount; ++i) { + await this.send(); // Allow exceptions to propagate out. + } + } + + public send() { + return new Promise((resolve, reject) => { + const start = process.hrtime(); + this.protocol.sendPayload({ + type: 'heartbeat', + serverTime: Date.now() + } as Payloads.Heartbeat, { + resolve: () => { + const hrInterval = process.hrtime(start); + const latency = hrInterval[0] * MS_PER_S + hrInterval[1] * MS_PER_NS; + this.protocol.conn.quality.latencyMs.update(latency); + resolve(latency); + }, + reject + }); + }); + } +} diff --git a/packages/sdk/src/protocols/index.ts b/packages/sdk/src/internal/protocols/index.ts similarity index 76% rename from packages/sdk/src/protocols/index.ts rename to packages/sdk/src/internal/protocols/index.ts index 2034d4677..4a8a06bb0 100644 --- a/packages/sdk/src/protocols/index.ts +++ b/packages/sdk/src/internal/protocols/index.ts @@ -1,10 +1,13 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -export * from './protocol'; -export * from './heartbeat'; -export * from './middleware'; -export * from './clientPreprocessing'; -export * from './serverPreprocessing'; +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +export * from './clientPreprocessing'; +export * from './execution'; +export * from './handshake'; +export * from './heartbeat'; +export * from './middleware'; +export * from './protocol'; +export * from './serverPreprocessing'; +export * from './sync'; diff --git a/packages/sdk/src/protocols/middleware.ts b/packages/sdk/src/internal/protocols/middleware.ts similarity index 81% rename from packages/sdk/src/protocols/middleware.ts rename to packages/sdk/src/internal/protocols/middleware.ts index e25096f8c..ba68dfec8 100644 --- a/packages/sdk/src/protocols/middleware.ts +++ b/packages/sdk/src/internal/protocols/middleware.ts @@ -1,20 +1,19 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -import { Message } from '..'; -import { ExportedPromise } from '../utils/exportedPromise'; - -/** - * @hidden - * Interface describing Protocol middleware. - * NOTE: This could be made more complex if needed, with leading edge/trailing edge support by passing a `next` - * function (restify style) - */ -export interface Middleware { - /** @optional */ - beforeSend?(message: Message, promise?: ExportedPromise): Message; - /** @optional */ - beforeRecv?(message: Message): Message; -} +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { ExportedPromise, Message } from '../../internal'; + +/** + * @hidden + * Interface describing Protocol middleware. + * NOTE: This could be made more complex if needed, with leading edge/trailing edge support by passing a `next` + * function (restify style) + */ +export interface Middleware { + /** @optional */ + beforeSend?(message: Message, promise?: ExportedPromise): Message; + /** @optional */ + beforeRecv?(message: Message): Message; +} diff --git a/packages/sdk/src/protocols/protocol.ts b/packages/sdk/src/internal/protocols/protocol.ts similarity index 88% rename from packages/sdk/src/protocols/protocol.ts rename to packages/sdk/src/internal/protocols/protocol.ts index d9710debf..61a1638ce 100644 --- a/packages/sdk/src/protocols/protocol.ts +++ b/packages/sdk/src/internal/protocols/protocol.ts @@ -1,211 +1,215 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -import { EventEmitter } from 'events'; -import { Connection, Guid, Message, newGuid } from '..'; -import { log } from '../log'; -import { Payload } from '../types/network/payloads'; -import { ExportedPromise } from '../utils/exportedPromise'; -import filterEmpty from '../utils/filterEmpty'; -import { Middleware } from './middleware'; - -/** - * @hidden - * Class to handle sending and receiving messages with a client. - */ -export class Protocol extends EventEmitter { - private middlewares: Middleware[] = []; - - private promise: Promise; - private promiseResolve: (value?: void | PromiseLike) => void; - private promiseReject: (reason?: any) => void; - - public get conn() { return this._conn; } - public get promises() { return this.conn.promises; } - public get name() { return this.constructor.name; } - - constructor(private _conn: Connection) { - super(); - this.onReceive = this.onReceive.bind(this); - this.onClose = this.onClose.bind(this); - this.promise = new Promise((resolve, reject) => { - this.promiseResolve = resolve; - this.promiseReject = reject; - }); - } - - public async run() { - try { - this.startListening(); - await this.completed(); - } catch (e) { - this.reject(e); - } - } - - public async completed() { - return this.promise; - } - - public use(middleware: Middleware) { - this.middlewares.push(middleware); - } - - public startListening() { - log.debug('network', `${this.name} started listening`); - this.conn.on('recv', this.onReceive); - this.conn.on('close', this.onClose); - } - - public stopListening() { - this.conn.off('recv', this.onReceive); - this.conn.off('close', this.onClose); - log.debug('network', `${this.name} stopped listening`); - } - - public sendPayload(payload: Partial, promise?: ExportedPromise) { - this.sendMessage({ payload }, promise); - } - - public sendMessage(message: Message, promise?: ExportedPromise, timeoutSeconds?: number) { - message.id = message.id ?? newGuid(); - - // Run message through all the middlewares - const middlewares = this.middlewares.slice(); - for (const middleware of middlewares) { - if (middleware.beforeSend) { - message = middleware.beforeSend(message, promise); - if (!message) { - if (promise && promise.reject) { - promise.reject(); - } - return; - } - } - } - - const setReplyTimeout = () => { - if (timeoutSeconds > 0) { - return setTimeout(() => { - const reason = `${this.name} timed out awaiting response for ${message.payload.type}, ` + - `id:${message.id}.`; - log.error('network', reason); - this.rejectPromiseForMessage(message.id, reason); - this.conn.close(); - }, timeoutSeconds * 1000); - } - }; - - // Save the reply callback - if (promise) { - this.promises.set(message.id, { - promise, - timeout: setReplyTimeout() - }); - } - - log.verbose('network', `${this.name} send id:${message.id.substr(0, 8)}, type:${message.payload.type}`); - log.verbose('network-content', JSON.stringify(message, (key, value) => filterEmpty(value))); - - this.conn.send(message); - } - - public recvMessage(message: Message) { - if (message.replyToId) { - log.verbose('network', `${this.name} recv id:${message.id.substr(0, 8)}, ` + - `replyTo:${message.replyToId.substr(0, 8)}, type:${message.payload.type}`); - } else { - log.verbose('network', `${this.name} recv id:${message.id.substr(0, 8)}, ` + - `type:${message.payload.type}`); - } - log.verbose('network-content', JSON.stringify(message, (key, value) => filterEmpty(value))); - - // Run message through all the middlewares - const middlewares = this.middlewares.slice(); - for (const middleware of middlewares) { - if (middleware.beforeRecv) { - message = middleware.beforeRecv(message); - if (!message) { - return; - } - } - } - - if (message.replyToId) { - this.handleReplyMessage(message); - } else { - this.recvPayload(message.payload); - } - } - - public recvPayload(payload: Partial) { - if (payload && payload.type && payload.type.length) { - const handler = (this as any)[`recv-${payload.type}`] || (() => { - log.error('network', `[ERROR] ${this.name} has no handler for payload ${payload.type}!`); - }); - handler(payload); - } else { - log.error('network', `[ERROR] ${this.name} invalid message payload!`); - } - } - - public drainPromises() { - if (Object.keys(this.promises).length) { - return new Promise((resolve, reject) => { - /* eslint-disable @typescript-eslint/no-use-before-define */ - const check = (): NodeJS.Timeout | void => Object.keys(this.promises).length ? set() : resolve(); - const set = () => setTimeout(() => check(), 10); - set(); - /* eslint-enable @typescript-eslint/no-use-before-define */ - // TODO: Would be better to not have to check on a timer here - }); - } - } - - protected resolve() { - this.stopListening(); - this.promiseResolve(); - } - - protected reject(e?: any) { - this.stopListening(); - this.promiseReject(e); - } - - protected handleReplyMessage(message: Message) { - const queuedPromise = this.promises.get(message.replyToId); - if (!queuedPromise) { - this.missingPromiseForReplyMessage(message); - } else { - this.promises.delete(message.replyToId); - clearTimeout(queuedPromise.timeout); - queuedPromise.promise.resolve(message.payload, message); - } - } - - private rejectPromiseForMessage(messageId: Guid, reason?: any) { - const queuedPromise = this.promises.get(messageId); - if (queuedPromise?.promise?.reject) { - try { clearTimeout(queuedPromise.timeout); } catch { } - try { this.promises.delete(messageId); } catch { } - try { queuedPromise.promise.reject(reason); } catch { } - } - } - - protected missingPromiseForReplyMessage(message: Message) { - log.error('network', `[ERROR] ${this.name} received unexpected reply message! ` + - `payload: ${message.payload.type}, replyToId: ${message.replyToId}`); - } - - private onReceive = (message: Message) => { - this.recvMessage(message); - }; - - private onClose = () => { - for (const id of this.promises.keys()) { - this.rejectPromiseForMessage(id, "Connection closed."); - } - }; -} +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { EventEmitter } from 'events'; + +import { Guid, log, newGuid } from '../..'; +import { + Connection, + ExportedPromise, + filterEmpty, + Message, + Payloads, + Protocols +} from '../../internal'; + +/** + * @hidden + * Class to handle sending and receiving messages with a client. + */ +export class Protocol extends EventEmitter { + private middlewares: Protocols.Middleware[] = []; + + private promise: Promise; + private promiseResolve: (value?: void | PromiseLike) => void; + private promiseReject: (reason?: any) => void; + + public get conn() { return this._conn; } + public get promises() { return this.conn.promises; } + public get name() { return this.constructor.name; } + + constructor(private _conn: Connection) { + super(); + this.onReceive = this.onReceive.bind(this); + this.onClose = this.onClose.bind(this); + this.promise = new Promise((resolve, reject) => { + this.promiseResolve = resolve; + this.promiseReject = reject; + }); + } + + public async run() { + try { + this.startListening(); + await this.completed(); + } catch (e) { + this.reject(e); + } + } + + public async completed() { + return this.promise; + } + + public use(middleware: Protocols.Middleware) { + this.middlewares.push(middleware); + } + + public startListening() { + log.debug('network', `${this.name} started listening`); + this.conn.on('recv', this.onReceive); + this.conn.on('close', this.onClose); + } + + public stopListening() { + this.conn.off('recv', this.onReceive); + this.conn.off('close', this.onClose); + log.debug('network', `${this.name} stopped listening`); + } + + public sendPayload(payload: Partial, promise?: ExportedPromise) { + this.sendMessage({ payload }, promise); + } + + public sendMessage(message: Message, promise?: ExportedPromise, timeoutSeconds?: number) { + message.id = message.id ?? newGuid(); + + // Run message through all the middlewares + const middlewares = this.middlewares.slice(); + for (const middleware of middlewares) { + if (middleware.beforeSend) { + message = middleware.beforeSend(message, promise); + if (!message) { + if (promise && promise.reject) { + promise.reject(); + } + return; + } + } + } + + const setReplyTimeout = () => { + if (timeoutSeconds > 0) { + return setTimeout(() => { + const reason = `${this.name} timed out awaiting response for ${message.payload.type}, ` + + `id:${message.id}.`; + log.error('network', reason); + this.rejectPromiseForMessage(message.id, reason); + this.conn.close(); + }, timeoutSeconds * 1000); + } + }; + + // Save the reply callback + if (promise) { + this.promises.set(message.id, { + promise, + timeout: setReplyTimeout() + }); + } + + log.verbose('network', `${this.name} send id:${message.id.substr(0, 8)}, type:${message.payload.type}`); + log.verbose('network-content', JSON.stringify(message, (key, value) => filterEmpty(value))); + + this.conn.send(message); + } + + public recvMessage(message: Message) { + if (message.replyToId) { + log.verbose('network', `${this.name} recv id:${message.id.substr(0, 8)}, ` + + `replyTo:${message.replyToId.substr(0, 8)}, type:${message.payload.type}`); + } else { + log.verbose('network', `${this.name} recv id:${message.id.substr(0, 8)}, ` + + `type:${message.payload.type}`); + } + log.verbose('network-content', JSON.stringify(message, (key, value) => filterEmpty(value))); + + // Run message through all the middlewares + const middlewares = this.middlewares.slice(); + for (const middleware of middlewares) { + if (middleware.beforeRecv) { + message = middleware.beforeRecv(message); + if (!message) { + return; + } + } + } + + if (message.replyToId) { + this.handleReplyMessage(message); + } else { + this.recvPayload(message.payload); + } + } + + public recvPayload(payload: Partial) { + if (payload && payload.type && payload.type.length) { + const handler = (this as any)[`recv-${payload.type}`] || (() => { + log.error('network', `[ERROR] ${this.name} has no handler for payload ${payload.type}!`); + }); + handler(payload); + } else { + log.error('network', `[ERROR] ${this.name} invalid message payload!`); + } + } + + public drainPromises() { + if (Object.keys(this.promises).length) { + return new Promise((resolve, reject) => { + /* eslint-disable @typescript-eslint/no-use-before-define */ + const check = (): NodeJS.Timeout | void => Object.keys(this.promises).length ? set() : resolve(); + const set = () => setTimeout(() => check(), 10); + set(); + /* eslint-enable @typescript-eslint/no-use-before-define */ + // TODO: Would be better to not have to check on a timer here + }); + } + } + + protected resolve() { + this.stopListening(); + this.promiseResolve(); + } + + protected reject(e?: any) { + this.stopListening(); + this.promiseReject(e); + } + + protected handleReplyMessage(message: Message) { + const queuedPromise = this.promises.get(message.replyToId); + if (!queuedPromise) { + this.missingPromiseForReplyMessage(message); + } else { + this.promises.delete(message.replyToId); + clearTimeout(queuedPromise.timeout); + queuedPromise.promise.resolve(message.payload, message); + } + } + + private rejectPromiseForMessage(messageId: Guid, reason?: any) { + const queuedPromise = this.promises.get(messageId); + if (queuedPromise?.promise?.reject) { + try { clearTimeout(queuedPromise.timeout); } catch { } + try { this.promises.delete(messageId); } catch { } + try { queuedPromise.promise.reject(reason); } catch { } + } + } + + protected missingPromiseForReplyMessage(message: Message) { + log.error('network', `[ERROR] ${this.name} received unexpected reply message! ` + + `payload: ${message.payload.type}, replyToId: ${message.replyToId}`); + } + + private onReceive = (message: Message) => { + this.recvMessage(message); + }; + + private onClose = () => { + for (const id of this.promises.keys()) { + this.rejectPromiseForMessage(id, "Connection closed."); + } + }; +} diff --git a/packages/sdk/src/protocols/serverPreprocessing.ts b/packages/sdk/src/internal/protocols/serverPreprocessing.ts similarity index 65% rename from packages/sdk/src/protocols/serverPreprocessing.ts rename to packages/sdk/src/internal/protocols/serverPreprocessing.ts index 860c67321..2068bdccc 100644 --- a/packages/sdk/src/protocols/serverPreprocessing.ts +++ b/packages/sdk/src/internal/protocols/serverPreprocessing.ts @@ -1,23 +1,21 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -import { Middleware } from '.'; -import { Message } from '..'; -import { ExportedPromise } from '../utils/exportedPromise'; - -/** - * @hidden - */ -export class ServerPreprocessing implements Middleware { - constructor() { - this.beforeSend = this.beforeSend.bind(this); - } - - /** @private */ - public beforeSend = (message: Message, promise?: ExportedPromise): Message => { - message.serverTimeMs = message.serverTimeMs || Date.now(); - return message; - }; -} +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { ExportedPromise, Message, Protocols } from '../../internal'; + +/** + * @hidden + */ +export class ServerPreprocessing implements Protocols.Middleware { + constructor() { + this.beforeSend = this.beforeSend.bind(this); + } + + /** @private */ + public beforeSend = (message: Message, promise?: ExportedPromise): Message => { + message.serverTimeMs = message.serverTimeMs || Date.now(); + return message; + }; +} diff --git a/packages/sdk/src/protocols/sync.ts b/packages/sdk/src/internal/protocols/sync.ts similarity index 64% rename from packages/sdk/src/protocols/sync.ts rename to packages/sdk/src/internal/protocols/sync.ts index 6fd135817..2cc53e75c 100644 --- a/packages/sdk/src/protocols/sync.ts +++ b/packages/sdk/src/internal/protocols/sync.ts @@ -1,27 +1,24 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -import { ServerPreprocessing } from '.'; -import { Connection } from '..'; -import * as Payloads from '../types/network/payloads'; -import { Protocol } from './protocol'; - -/** - * @hidden - * Class to manage the join process with a client. - */ -export class Sync extends Protocol { - constructor(conn: Connection) { - super(conn); - // Behave like a server-side endpoint (send heartbeats, measure connection quality) - this.use(new ServerPreprocessing()); - } - - /** @override */ - public startListening() { - super.sendPayload({ type: 'sync-complete' } as Payloads.SyncComplete); - process.nextTick(() => { this.resolve(); }); - } -} +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { Connection, Payloads, Protocols } from '../../internal'; + +/** + * @hidden + * Class to manage the join process with a client. + */ +export class Sync extends Protocols.Protocol { + constructor(conn: Connection) { + super(conn); + // Behave like a server-side endpoint (send heartbeats, measure connection quality) + this.use(new Protocols.ServerPreprocessing()); + } + + /** @override */ + public startListening() { + super.sendPayload({ type: 'sync-complete' } as Payloads.SyncComplete); + process.nextTick(() => { this.resolve(); }); + } +} diff --git a/packages/sdk/src/utils/queuedPromise.ts b/packages/sdk/src/internal/queuedPromise.ts similarity index 76% rename from packages/sdk/src/utils/queuedPromise.ts rename to packages/sdk/src/internal/queuedPromise.ts index f47f667db..d522c6663 100644 --- a/packages/sdk/src/utils/queuedPromise.ts +++ b/packages/sdk/src/internal/queuedPromise.ts @@ -1,12 +1,12 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -import { ExportedPromise } from './exportedPromise'; - -/** @hidden */ -export interface QueuedPromise { - promise: ExportedPromise; - timeout: NodeJS.Timer; -} +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { ExportedPromise } from '../internal'; + +/** @hidden */ +export interface QueuedPromise { + promise: ExportedPromise; + timeout: NodeJS.Timer; +} diff --git a/packages/sdk/src/types/network/subscriptionType.ts b/packages/sdk/src/internal/subscriptionType.ts similarity index 97% rename from packages/sdk/src/types/network/subscriptionType.ts rename to packages/sdk/src/internal/subscriptionType.ts index cd245ce5b..6b27266c1 100644 --- a/packages/sdk/src/types/network/subscriptionType.ts +++ b/packages/sdk/src/internal/subscriptionType.ts @@ -1,10 +1,10 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -/** - * A type representing the different parts of an Actor that can be subscribed to. When subscribed, the Actor will - * receive updates when corresponding changes occur on the host. - */ -export type SubscriptionType = 'transform' | 'rigidbody' | 'collider'; +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +/** + * A type representing the different parts of an Actor that can be subscribed to. When subscribed, the Actor will + * receive updates when corresponding changes occur on the host. + */ +export type SubscriptionType = 'transform' | 'rigidbody' | 'collider'; diff --git a/packages/sdk/src/types/network/trace.ts b/packages/sdk/src/internal/trace.ts similarity index 94% rename from packages/sdk/src/types/network/trace.ts rename to packages/sdk/src/internal/trace.ts index d0baae408..ee2493d94 100644 --- a/packages/sdk/src/types/network/trace.ts +++ b/packages/sdk/src/internal/trace.ts @@ -1,17 +1,17 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -/** - * Defines different log severity levels. - */ -export type TraceSeverity = 'debug' | 'info' | 'warning' | 'error'; - -/** - * @hidden - */ -export interface Trace { - severity: TraceSeverity; - message: string; -} +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +/** + * Defines different log severity levels. + */ +export type TraceSeverity = 'debug' | 'info' | 'warning' | 'error'; + +/** + * @hidden + */ +export interface Trace { + severity: TraceSeverity; + message: string; +} diff --git a/packages/sdk/src/utils/deterministicGuids.ts b/packages/sdk/src/internal/util/deterministicGuids.ts similarity index 96% rename from packages/sdk/src/utils/deterministicGuids.ts rename to packages/sdk/src/internal/util/deterministicGuids.ts index 37a586023..e5551dce0 100644 --- a/packages/sdk/src/utils/deterministicGuids.ts +++ b/packages/sdk/src/internal/util/deterministicGuids.ts @@ -1,48 +1,48 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -import * as crypto from 'crypto'; - -// Map for byte <-> hex string conversion -const byteToHex: string[] = []; -for (let i = 0; i < 256; i++) { - byteToHex[i] = (i + 0x100).toString(16).substr(1); -} - -function uuidParse(buf: ArrayLike): string { - let i = 0; - const bth = byteToHex; - return ( - bth[buf[i++]] + bth[buf[i++]] + - bth[buf[i++]] + bth[buf[i++]] + '-' + - bth[buf[i++]] + bth[buf[i++]] + '-' + - bth[buf[i++]] + bth[buf[i++]] + '-' + - bth[buf[i++]] + bth[buf[i++]] + '-' + - bth[buf[i++]] + bth[buf[i++]] + - bth[buf[i++]] + bth[buf[i++]] + - bth[buf[i++]] + bth[buf[i++]]); -} - -/** - * @hidden - * Class for generating a sequence of deterministic GUID values. - * NOTE: This is a quick hack, and does not generate valid UUIDs. - * To generate a deterministic sequence of values that are also valid - * UUIDs, we must employ the "Name-based UUID" method described in - * RFC 4122 §4.3 (http://www.ietf.org/rfc/rfc4122.txt), which is - * supported by Node's 'uuid/v3' module. - */ -export class DeterministicGuids { - constructor(private seed: string) { - } - public next(): string { - const result = this.seed; - const hashedBytes = crypto.createHash('sha1').update(this.seed, 'ascii').digest(); - const sizedBytes = new Buffer(16); - sizedBytes.set(hashedBytes); - this.seed = uuidParse(sizedBytes); - return result; - } -} +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import * as crypto from 'crypto'; + +// Map for byte <-> hex string conversion +const byteToHex: string[] = []; +for (let i = 0; i < 256; i++) { + byteToHex[i] = (i + 0x100).toString(16).substr(1); +} + +function uuidParse(buf: ArrayLike): string { + let i = 0; + const bth = byteToHex; + return ( + bth[buf[i++]] + bth[buf[i++]] + + bth[buf[i++]] + bth[buf[i++]] + '-' + + bth[buf[i++]] + bth[buf[i++]] + '-' + + bth[buf[i++]] + bth[buf[i++]] + '-' + + bth[buf[i++]] + bth[buf[i++]] + '-' + + bth[buf[i++]] + bth[buf[i++]] + + bth[buf[i++]] + bth[buf[i++]] + + bth[buf[i++]] + bth[buf[i++]]); +} + +/** + * @hidden + * Class for generating a sequence of deterministic GUID values. + * NOTE: This is a quick hack, and does not generate valid UUIDs. + * To generate a deterministic sequence of values that are also valid + * UUIDs, we must employ the "Name-based UUID" method described in + * RFC 4122 §4.3 (http://www.ietf.org/rfc/rfc4122.txt), which is + * supported by Node's 'uuid/v3' module. + */ +export class DeterministicGuids { + constructor(private seed: string) { + } + public next(): string { + const result = this.seed; + const hashedBytes = crypto.createHash('sha1').update(this.seed, 'ascii').digest(); + const sizedBytes = new Buffer(16); + sizedBytes.set(hashedBytes); + this.seed = uuidParse(sizedBytes); + return result; + } +} diff --git a/packages/sdk/src/utils/exponentialMovingAverage.ts b/packages/sdk/src/internal/util/exponentialMovingAverage.ts similarity index 96% rename from packages/sdk/src/utils/exponentialMovingAverage.ts rename to packages/sdk/src/internal/util/exponentialMovingAverage.ts index 32cf79e34..e4548cf09 100644 --- a/packages/sdk/src/utils/exponentialMovingAverage.ts +++ b/packages/sdk/src/internal/util/exponentialMovingAverage.ts @@ -1,21 +1,21 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -/** - * @hidden - * Computes an Exponentially Weighted Moving Average (EWMA). - * https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average - */ -export class ExponentialMovingAverage { - public alpha = 0.75; - public value = 0; - - /** Computes the latest value given a new sample */ - public update(v: number): void { - if (typeof v === 'number') { - this.value = this.alpha * v + (1 - this.alpha) * this.value; - } - } -} +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +/** + * @hidden + * Computes an Exponentially Weighted Moving Average (EWMA). + * https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average + */ +export class ExponentialMovingAverage { + public alpha = 0.75; + public value = 0; + + /** Computes the latest value given a new sample */ + public update(v: number): void { + if (typeof v === 'number') { + this.value = this.alpha * v + (1 - this.alpha) * this.value; + } + } +} diff --git a/packages/sdk/src/utils/filterEmpty.ts b/packages/sdk/src/internal/util/filterEmpty.ts similarity index 86% rename from packages/sdk/src/utils/filterEmpty.ts rename to packages/sdk/src/internal/util/filterEmpty.ts index cfbac0d24..c05893f77 100644 --- a/packages/sdk/src/utils/filterEmpty.ts +++ b/packages/sdk/src/internal/util/filterEmpty.ts @@ -7,7 +7,7 @@ * @hidden * If `obj` is an empty object, return undefined. */ -export default function filterEmpty(obj: any) { +export function filterEmpty(obj: any) { if (typeof obj === 'object' && obj !== null && !Object.keys(obj).length) { return undefined; } else { diff --git a/packages/sdk/src/internal/util/index.ts b/packages/sdk/src/internal/util/index.ts new file mode 100644 index 000000000..7829e5e7f --- /dev/null +++ b/packages/sdk/src/internal/util/index.ts @@ -0,0 +1,16 @@ +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +export * from './deterministicGuids'; +export * from './exponentialMovingAverage'; +export * from './filterEmpty'; +export * from './observe'; +export * from './readPath'; +export * from './resolveJsonValues'; +export * from './safeAccessPath'; +export * from './trackingClock'; +export * from './validateJsonFieldName'; +export * from './verifyClient'; +export * from './visitActor'; diff --git a/packages/sdk/src/utils/observe.ts b/packages/sdk/src/internal/util/observe.ts similarity index 100% rename from packages/sdk/src/utils/observe.ts rename to packages/sdk/src/internal/util/observe.ts diff --git a/packages/sdk/src/utils/parseNetworkLogs.ts b/packages/sdk/src/internal/util/parseNetworkLogs.ts similarity index 98% rename from packages/sdk/src/utils/parseNetworkLogs.ts rename to packages/sdk/src/internal/util/parseNetworkLogs.ts index c3e50f6f1..a75266559 100644 --- a/packages/sdk/src/utils/parseNetworkLogs.ts +++ b/packages/sdk/src/internal/util/parseNetworkLogs.ts @@ -8,7 +8,7 @@ import { resolve } from 'path'; import { promisify } from 'util'; const readFile = promisify(readFileNodeAsync); -import safeAccessPath from './safeAccessPath'; +import { safeAccessPath } from '../../internal'; /* eslint-disable no-console */ diff --git a/packages/sdk/src/utils/readPath.ts b/packages/sdk/src/internal/util/readPath.ts similarity index 80% rename from packages/sdk/src/utils/readPath.ts rename to packages/sdk/src/internal/util/readPath.ts index 06c108eba..e3a9f7d25 100644 --- a/packages/sdk/src/utils/readPath.ts +++ b/packages/sdk/src/internal/util/readPath.ts @@ -3,13 +3,13 @@ * Licensed under the MIT License. */ -import validateJsonFieldName from "./validateJsonFieldName"; +import { validateJsonFieldName } from "../../internal"; /** * @hidden * Reads the value at the path in the src object and writes it to the dst object. */ -export default function readPath(src: any, dst: any, ...path: string[]) { +export function readPath(src: any, dst: any, ...path: string[]) { let field; while (path.length) { field = path.shift(); diff --git a/packages/sdk/src/utils/resolveJsonValues.ts b/packages/sdk/src/internal/util/resolveJsonValues.ts similarity index 91% rename from packages/sdk/src/utils/resolveJsonValues.ts rename to packages/sdk/src/internal/util/resolveJsonValues.ts index ad81d8080..cdcaf01a2 100644 --- a/packages/sdk/src/utils/resolveJsonValues.ts +++ b/packages/sdk/src/internal/util/resolveJsonValues.ts @@ -8,7 +8,7 @@ * Recursively look for values with a `toJSON()` method. If found, * call it and replace the value with the return value. */ -export default function resolveJsonValues(obj: any) { +export function resolveJsonValues(obj: any) { if (typeof obj === 'object') { if (typeof obj.toJSON === 'function') { obj = obj.toJSON(); diff --git a/packages/sdk/src/utils/safeAccessPath.ts b/packages/sdk/src/internal/util/safeAccessPath.ts similarity index 75% rename from packages/sdk/src/utils/safeAccessPath.ts rename to packages/sdk/src/internal/util/safeAccessPath.ts index d187d89b3..83fa9d38e 100644 --- a/packages/sdk/src/utils/safeAccessPath.ts +++ b/packages/sdk/src/internal/util/safeAccessPath.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. */ -export default function safeAccessPath(obj: any, ...path: any[]): any { +export function safeAccessPath(obj: any, ...path: any[]): any { for (const part of path) { if (!obj[part]) { return undefined; diff --git a/packages/sdk/src/utils/trackingClock.ts b/packages/sdk/src/internal/util/trackingClock.ts similarity index 95% rename from packages/sdk/src/utils/trackingClock.ts rename to packages/sdk/src/internal/util/trackingClock.ts index f034a562a..fb352e194 100644 --- a/packages/sdk/src/utils/trackingClock.ts +++ b/packages/sdk/src/internal/util/trackingClock.ts @@ -1,38 +1,38 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -/** - * @hidden - * Clock to estimate the current server time from a known sample. - */ -export class TrackingClock { - private sampleMs = 0; - private sampleTimeMs = Date.now(); - - /** - * Returns the current server time in milliseconds, estimated from last known value. - */ - public get nowMs() { - if (this.sampleTimeMs > 0) { - const currentTimeMs = Date.now(); - const timespanMs = currentTimeMs - this.sampleTimeMs; - const estimatedTimeMs = (this.sampleMs + timespanMs); - return estimatedTimeMs; - } else { - return 0; - } - } - - /** - * Updates the last known server time. - * @param valueMs The value, in milliseconds. - */ - public update(valueMs: number) { - if (valueMs > this.sampleMs) { - this.sampleMs = valueMs; - this.sampleTimeMs = Date.now(); - } - } -} +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +/** + * @hidden + * Clock to estimate the current server time from a known sample. + */ +export class TrackingClock { + private sampleMs = 0; + private sampleTimeMs = Date.now(); + + /** + * Returns the current server time in milliseconds, estimated from last known value. + */ + public get nowMs() { + if (this.sampleTimeMs > 0) { + const currentTimeMs = Date.now(); + const timespanMs = currentTimeMs - this.sampleTimeMs; + const estimatedTimeMs = (this.sampleMs + timespanMs); + return estimatedTimeMs; + } else { + return 0; + } + } + + /** + * Updates the last known server time. + * @param valueMs The value, in milliseconds. + */ + public update(valueMs: number) { + if (valueMs > this.sampleMs) { + this.sampleMs = valueMs; + this.sampleTimeMs = Date.now(); + } + } +} diff --git a/packages/sdk/src/utils/validateJsonFieldName.ts b/packages/sdk/src/internal/util/validateJsonFieldName.ts similarity index 86% rename from packages/sdk/src/utils/validateJsonFieldName.ts rename to packages/sdk/src/internal/util/validateJsonFieldName.ts index 436593613..888a1ef16 100644 --- a/packages/sdk/src/utils/validateJsonFieldName.ts +++ b/packages/sdk/src/internal/util/validateJsonFieldName.ts @@ -8,7 +8,7 @@ * Verifies that `key` isn't an invalid key name. Useful for detecting when we're leaking private * fields into network payloads. */ -export default function validateJsonFieldName(key: string) { +export function validateJsonFieldName(key: string) { // Uncomment to validate JSON payloads /* if (key.startsWith('_')) { diff --git a/packages/sdk/src/utils/verifyClient.ts b/packages/sdk/src/internal/util/verifyClient.ts similarity index 94% rename from packages/sdk/src/utils/verifyClient.ts rename to packages/sdk/src/internal/util/verifyClient.ts index fe275aac1..878f0122e 100644 --- a/packages/sdk/src/utils/verifyClient.ts +++ b/packages/sdk/src/internal/util/verifyClient.ts @@ -1,88 +1,88 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -import semver from 'semver'; -import * as Constants from '../constants'; -import { log } from '../log'; - -/* - * Current SDK Version - Read from package.json. - */ -const CurrentSDKVersion = semver.coerce(require('../../package.json').version); - -/* - * Minimum Supported Client Library version - * As part of the manual SDK release procedures, this is reset to match CurrentSDKVersion Major.Minor if new functions - * have been added that don't work on older clients (i.e. pretty much every release). Since host apps are required to - * update client libraries regularly, this one is not a big deal to update. - */ -const MinimumSupportedClientVersion = semver.coerce('0.15'); - -/** - * @hidden - * 'ws' middleware to validate the client protocol version when processing a connection upgrade request. - * @param info 'ws' request information - * @param cb 'ws' verification callback - */ -export default function verifyClient( - info: any, cb: (verified: boolean, code?: number, message?: string) => any): any { - // Look for the upgrade request. - const req = info.req || {}; - - // Look for the request headers. - const headers = req.headers || []; - - // Verify minimum supported versions are met (client and SDK). - - /* - * Due to a shortcoming in C# ClientWebSocket, we have no way to convey any error details in the HTTP response, - * including error code. - * See: "ClientWebSocket does not provide upgrade request error details" - * https://github.com/dotnet/corefx/issues/29163 - */ - - const CurrentClientVersion - = semver.coerce(decodeURIComponent(headers[Constants.HTTPHeaders.CurrentClientVersion])); - const MinimumSupportedSDKVersion - = semver.coerce(decodeURIComponent(headers[Constants.HTTPHeaders.MinimumSupportedSDKVersion])); - - if (CurrentClientVersion && MinimumSupportedSDKVersion) { - // Test the current client version. Is it greater than or equal to the minimum client version? - const clientPass = semver.gte(CurrentClientVersion, MinimumSupportedClientVersion); - // Test the current SDK version. Is it greater than or equal to the minimum SDK version? - const sdkPass = semver.gte(CurrentSDKVersion, MinimumSupportedSDKVersion); - - if (!clientPass) { - const message = `Connection rejected due to out of date client. ` + - `Client version: ${CurrentClientVersion.toString()}. ` + - `Min supported version by SDK: ${MinimumSupportedClientVersion.toString()}`; - log.info('app', message); - return cb(false, 403, message); - } - - if (!sdkPass) { - const message = `Connection rejected due to out of date SDK. ` + - `Current SDK version: ${CurrentSDKVersion.toString()}. ` + - `Min supported version by client: ${MinimumSupportedSDKVersion.toString()}`; - log.info('app', message); - // Log this line to the console. The developer should know about this. - // eslint-disable-next-line no-console - console.info(message); - return cb(false, 403, message); - } - - // Client and SDK are compatible. - return cb(true); - } - - // Temporary: Support old clients reporting the legacy protocol version. - // TODO: Remove this after a few releases. - const legacyProtocolVersion = decodeURIComponent(headers[Constants.HTTPHeaders.LegacyProtocolVersion]); - if (legacyProtocolVersion === '1') { - return cb(true); - } - - return cb(false, 403, "Version headers missing."); -} +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import semver from 'semver'; +import { Constants } from '../../internal'; +import { log } from '../..'; + +/* + * Current SDK Version - Read from package.json. + */ +const CurrentSDKVersion = semver.coerce(require('../../package.json').version); + +/* + * Minimum Supported Client Library version + * As part of the manual SDK release procedures, this is reset to match CurrentSDKVersion Major.Minor if new functions + * have been added that don't work on older clients (i.e. pretty much every release). Since host apps are required to + * update client libraries regularly, this one is not a big deal to update. + */ +const MinimumSupportedClientVersion = semver.coerce('0.15'); + +/** + * @hidden + * 'ws' middleware to validate the client protocol version when processing a connection upgrade request. + * @param info 'ws' request information + * @param cb 'ws' verification callback + */ +export function verifyClient( + info: any, cb: (verified: boolean, code?: number, message?: string) => any): any { + // Look for the upgrade request. + const req = info.req || {}; + + // Look for the request headers. + const headers = req.headers || []; + + // Verify minimum supported versions are met (client and SDK). + + /* + * Due to a shortcoming in C# ClientWebSocket, we have no way to convey any error details in the HTTP response, + * including error code. + * See: "ClientWebSocket does not provide upgrade request error details" + * https://github.com/dotnet/corefx/issues/29163 + */ + + const CurrentClientVersion + = semver.coerce(decodeURIComponent(headers[Constants.HTTPHeaders.CurrentClientVersion])); + const MinimumSupportedSDKVersion + = semver.coerce(decodeURIComponent(headers[Constants.HTTPHeaders.MinimumSupportedSDKVersion])); + + if (CurrentClientVersion && MinimumSupportedSDKVersion) { + // Test the current client version. Is it greater than or equal to the minimum client version? + const clientPass = semver.gte(CurrentClientVersion, MinimumSupportedClientVersion); + // Test the current SDK version. Is it greater than or equal to the minimum SDK version? + const sdkPass = semver.gte(CurrentSDKVersion, MinimumSupportedSDKVersion); + + if (!clientPass) { + const message = `Connection rejected due to out of date client. ` + + `Client version: ${CurrentClientVersion.toString()}. ` + + `Min supported version by SDK: ${MinimumSupportedClientVersion.toString()}`; + log.info('app', message); + return cb(false, 403, message); + } + + if (!sdkPass) { + const message = `Connection rejected due to out of date SDK. ` + + `Current SDK version: ${CurrentSDKVersion.toString()}. ` + + `Min supported version by client: ${MinimumSupportedSDKVersion.toString()}`; + log.info('app', message); + // Log this line to the console. The developer should know about this. + // eslint-disable-next-line no-console + console.info(message); + return cb(false, 403, message); + } + + // Client and SDK are compatible. + return cb(true); + } + + // Temporary: Support old clients reporting the legacy protocol version. + // TODO: Remove this after a few releases. + const legacyProtocolVersion = decodeURIComponent(headers[Constants.HTTPHeaders.LegacyProtocolVersion]); + if (legacyProtocolVersion === '1') { + return cb(true); + } + + return cb(false, 403, "Version headers missing."); +} diff --git a/packages/sdk/src/utils/visitActor.ts b/packages/sdk/src/internal/util/visitActor.ts similarity index 84% rename from packages/sdk/src/utils/visitActor.ts rename to packages/sdk/src/internal/util/visitActor.ts index a8ff4a4a6..fb48157a5 100644 --- a/packages/sdk/src/utils/visitActor.ts +++ b/packages/sdk/src/internal/util/visitActor.ts @@ -1,12 +1,12 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -import { Actor } from "../types/runtime"; - -/** @hidden */ -export default function VisitActor(actor: Actor, callback: (actor: Actor) => void) { - actor.children.forEach(child => VisitActor(child, callback)); - callback(actor); -} +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { Actor } from "../.."; + +/** @hidden */ +export default function VisitActor(actor: Actor, callback: (actor: Actor) => void) { + actor.children.forEach(child => VisitActor(child, callback)); + callback(actor); +} diff --git a/packages/sdk/src/types/lookatMode.ts b/packages/sdk/src/types/lookatMode.ts deleted file mode 100644 index 3580379c9..000000000 --- a/packages/sdk/src/types/lookatMode.ts +++ /dev/null @@ -1,25 +0,0 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -/** - * Describes the ways in which an actor can face (point its local +Z axis toward) and track another object in the scene - */ -export enum LookAtMode { - - /** - * Actor is world-locked and does not rotate - */ - None = 'None', - - /** - * Actor rotates around its Y axis to face the target, offset by its rotation - */ - TargetY = 'TargetY', - - /** - * Actor rotates around its X and Y axes to face the target, offset by its rotation - */ - TargetXY = 'TargetXY' -} diff --git a/packages/sdk/src/types/network/index.ts b/packages/sdk/src/types/network/index.ts deleted file mode 100644 index f5f2e6e35..000000000 --- a/packages/sdk/src/types/network/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -export * from './message'; -export * from './trace'; -export * from './operationResultCode'; -export * from './subscriptionType'; diff --git a/packages/sdk/src/types/runtime/groupMask.ts b/packages/sdk/src/user/groupMask.ts similarity index 99% rename from packages/sdk/src/types/runtime/groupMask.ts rename to packages/sdk/src/user/groupMask.ts index 1f65461a0..3ad0f1e26 100644 --- a/packages/sdk/src/types/runtime/groupMask.ts +++ b/packages/sdk/src/user/groupMask.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ -import { Context } from '.'; +import { Context } from '..'; /** * A set of user group IDs. User groups are used to selectively enable several different diff --git a/packages/sdk/src/rpc/index.ts b/packages/sdk/src/user/index.ts similarity index 66% rename from packages/sdk/src/rpc/index.ts rename to packages/sdk/src/user/index.ts index d8806f299..95e290f4a 100644 --- a/packages/sdk/src/rpc/index.ts +++ b/packages/sdk/src/user/index.ts @@ -1,6 +1,7 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -export * from './rpc'; +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +export * from './groupMask'; +export * from './user'; diff --git a/packages/sdk/src/types/runtime/user.ts b/packages/sdk/src/user/user.ts similarity index 88% rename from packages/sdk/src/types/runtime/user.ts rename to packages/sdk/src/user/user.ts index d93559067..1b447f083 100644 --- a/packages/sdk/src/types/runtime/user.ts +++ b/packages/sdk/src/user/user.ts @@ -1,126 +1,125 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -import { Context, GroupMask, Guid } from '../..'; -import readPath from '../../utils/readPath'; -import { InternalUser } from '../internal/user'; -import { Patchable } from '../patchable'; - -export interface UserLike { - id: Guid; - name: string; - groups: number | GroupMask; - properties: { [name: string]: string }; -} - -/** - * The structure returned from [[User.prompt]]. - */ -export type DialogResponse = { - /** Whether the user replied in the positive (OK/Accept) or in the negative (Cancel). */ - submitted: boolean; - /** The string provided by the user in the dialog text input field. */ - text?: string; -}; - -export class User implements UserLike, Patchable { - private _internal: InternalUser; - /** @hidden */ - public get internal() { return this._internal; } - - private _name: string; - private _properties: { [name: string]: string }; - private _groups: GroupMask; - - public get context() { return this._context; } - public get id() { return this._id; } - public get name() { return this._name; } - - /** - * This user's group memberships. Some actors will behave differently depending on - * if the user is in at least one of a set of groups. See [[GroupMask]]. - */ - public get groups() { - if (!this._groups) { - this._groups = new GroupMask(this._context); - this._groups.allowDefault = false; - this._groups.onChanged(() => this.userChanged('groups')); - } - return this._groups; - } - public set groups(val) { - if (!val) { - if (this._groups) { - this._groups.clear(); - } - return; - } - - this._groups = val.getClean(); - this._groups.allowDefault = false; - this._groups.onChanged(() => this.userChanged('groups')); - this.userChanged('groups'); - } - - /** - * A grab bag of miscellaneous, possibly host-dependent, properties. - */ - public get properties() { return Object.freeze({ ...this._properties }); } - - /** - * PUBLIC METHODS - */ - - constructor(private _context: Context, private _id: Guid) { - this._internal = new InternalUser(this, this._context.internal); - } - - /** - * Present the user with a modal dialog, and resolve with the response. - * @param text A message presented to the user. - * @param acceptInput Whether or not the dialog should include a text input field. - */ - public prompt(text: string, acceptInput = false): Promise { - return this.internal.prompt(text, acceptInput); - } - - public copy(from: Partial): this { - // Pause change detection while we copy the values into the actor. - const wasObserving = this.internal.observing; - this.internal.observing = false; - - if (!from) { return this; } - if (from.id !== undefined) { this._id = from.id; } - if (from.name !== undefined) { this._name = from.name; } - if (from.properties !== undefined) { this._properties = from.properties; } - if (from.groups !== undefined) { - if (typeof from.groups === 'number') { - this.groups.setPacked(from.groups); - } else { - this.groups = from.groups; - } - } - - this.internal.observing = wasObserving; - return this; - } - - public toJSON() { - return { - id: this.id, - name: this.name, - groups: this.groups.packed(), - properties: this.properties, - } as UserLike; - } - - private userChanged(...path: string[]) { - if (this.internal.observing) { - this.internal.patch = this.internal.patch || {} as UserLike; - readPath(this, this.internal.patch, ...path); - this.context.internal.incrementGeneration(); - } - } -} +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { Context, GroupMask, Guid } from '..'; +import { Patchable, readPath, } from '../internal'; +import { UserInternal } from './userinternal'; + +export interface UserLike { + id: Guid; + name: string; + groups: number | GroupMask; + properties: { [name: string]: string }; +} + +/** + * The structure returned from [[User.prompt]]. + */ +export type DialogResponse = { + /** Whether the user replied in the positive (OK/Accept) or in the negative (Cancel). */ + submitted: boolean; + /** The string provided by the user in the dialog text input field. */ + text?: string; +}; + +export class User implements UserLike, Patchable { + private _internal: UserInternal; + /** @hidden */ + public get internal() { return this._internal; } + + private _name: string; + private _properties: { [name: string]: string }; + private _groups: GroupMask; + + public get context() { return this._context; } + public get id() { return this._id; } + public get name() { return this._name; } + + /** + * This user's group memberships. Some actors will behave differently depending on + * if the user is in at least one of a set of groups. See [[GroupMask]]. + */ + public get groups() { + if (!this._groups) { + this._groups = new GroupMask(this._context); + this._groups.allowDefault = false; + this._groups.onChanged(() => this.userChanged('groups')); + } + return this._groups; + } + public set groups(val) { + if (!val) { + if (this._groups) { + this._groups.clear(); + } + return; + } + + this._groups = val.getClean(); + this._groups.allowDefault = false; + this._groups.onChanged(() => this.userChanged('groups')); + this.userChanged('groups'); + } + + /** + * A grab bag of miscellaneous, possibly host-dependent, properties. + */ + public get properties() { return Object.freeze({ ...this._properties }); } + + /** + * PUBLIC METHODS + */ + + constructor(private _context: Context, private _id: Guid) { + this._internal = new UserInternal(this, this._context.internal); + } + + /** + * Present the user with a modal dialog, and resolve with the response. + * @param text A message presented to the user. + * @param acceptInput Whether or not the dialog should include a text input field. + */ + public prompt(text: string, acceptInput = false): Promise { + return this.internal.prompt(text, acceptInput); + } + + public copy(from: Partial): this { + // Pause change detection while we copy the values into the actor. + const wasObserving = this.internal.observing; + this.internal.observing = false; + + if (!from) { return this; } + if (from.id !== undefined) { this._id = from.id; } + if (from.name !== undefined) { this._name = from.name; } + if (from.properties !== undefined) { this._properties = from.properties; } + if (from.groups !== undefined) { + if (typeof from.groups === 'number') { + this.groups.setPacked(from.groups); + } else { + this.groups = from.groups; + } + } + + this.internal.observing = wasObserving; + return this; + } + + public toJSON() { + return { + id: this.id, + name: this.name, + groups: this.groups.packed(), + properties: this.properties, + } as UserLike; + } + + private userChanged(...path: string[]) { + if (this.internal.observing) { + this.internal.patch = this.internal.patch || {} as UserLike; + readPath(this, this.internal.patch, ...path); + this.context.internal.incrementGeneration(); + } + } +} diff --git a/packages/sdk/src/types/internal/user.ts b/packages/sdk/src/user/userInternal.ts similarity index 71% rename from packages/sdk/src/types/internal/user.ts rename to packages/sdk/src/user/userInternal.ts index 2de3d77f4..31ce8a4dc 100644 --- a/packages/sdk/src/types/internal/user.ts +++ b/packages/sdk/src/user/userInternal.ts @@ -1,53 +1,52 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -import { DialogResponse, User, UserLike } from '../..'; -import * as Payloads from '../network/payloads'; -import { InternalPatchable } from '../patchable'; -import { InternalContext } from './context'; - -/** - * @hidden - */ -export class InternalUser implements InternalPatchable { - public __rpc: any; - public observing = true; - public patch: UserLike; - - constructor(public user: User, public context: InternalContext) { - } - - public getPatchAndReset(): UserLike { - const patch = this.patch; - if (patch) { - patch.id = this.user.id; - delete this.patch; - } - return patch; - } - - public prompt(text: string, acceptInput: boolean): Promise { - const payload = { - type: 'show-dialog', - userId: this.user.id, - text, - acceptInput - } as Payloads.ShowDialog; - - return new Promise((resolve, reject) => { - this.context.sendPayload(payload, { resolve, reject }); - }) - .then(response => { - if (response.failureMessage) { - return Promise.reject(response.failureMessage); - } else { - return Promise.resolve({ - submitted: response.submitted, - text: response.text - } as DialogResponse); - } - }); - } -} +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { DialogResponse, User, UserLike } from '..'; +import { InternalPatchable, Payloads } from '../internal'; +import { ContextInternal } from '../core/contextInternal'; + +/** + * @hidden + */ +export class UserInternal implements InternalPatchable { + public __rpc: any; + public observing = true; + public patch: UserLike; + + constructor(public user: User, public context: ContextInternal) { + } + + public getPatchAndReset(): UserLike { + const patch = this.patch; + if (patch) { + patch.id = this.user.id; + delete this.patch; + } + return patch; + } + + public prompt(text: string, acceptInput: boolean): Promise { + const payload = { + type: 'show-dialog', + userId: this.user.id, + text, + acceptInput + } as Payloads.ShowDialog; + + return new Promise((resolve, reject) => { + this.context.sendPayload(payload, { resolve, reject }); + }) + .then(response => { + if (response.failureMessage) { + return Promise.reject(response.failureMessage); + } else { + return Promise.resolve({ + submitted: response.submitted, + text: response.text + } as DialogResponse); + } + }); + } +} diff --git a/packages/sdk/src/types/guid.ts b/packages/sdk/src/util/guid.ts similarity index 100% rename from packages/sdk/src/types/guid.ts rename to packages/sdk/src/util/guid.ts diff --git a/packages/sdk/src/adapters/index.ts b/packages/sdk/src/util/index.ts similarity index 50% rename from packages/sdk/src/adapters/index.ts rename to packages/sdk/src/util/index.ts index 399018ad2..c1644d29c 100644 --- a/packages/sdk/src/adapters/index.ts +++ b/packages/sdk/src/util/index.ts @@ -1,8 +1,9 @@ -/*! - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -export * from './adapter'; -export * from './websocket'; -export * from './multipeer'; +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +export * from './guid'; +export * from './log'; +export * from './readonlyMap'; +export * from './webHost'; diff --git a/packages/sdk/src/log.ts b/packages/sdk/src/util/log.ts similarity index 100% rename from packages/sdk/src/log.ts rename to packages/sdk/src/util/log.ts diff --git a/packages/sdk/src/types/readonlyMap.ts b/packages/sdk/src/util/readonlyMap.ts similarity index 100% rename from packages/sdk/src/types/readonlyMap.ts rename to packages/sdk/src/util/readonlyMap.ts diff --git a/packages/sdk/src/webHost.ts b/packages/sdk/src/util/webHost.ts similarity index 97% rename from packages/sdk/src/webHost.ts rename to packages/sdk/src/util/webHost.ts index e14312c2f..3d9e058e0 100644 --- a/packages/sdk/src/webHost.ts +++ b/packages/sdk/src/util/webHost.ts @@ -3,11 +3,11 @@ * Licensed under the MIT License. */ +import { resolve as urlResolve } from 'url'; import * as Restify from 'restify'; -import { Adapter, MultipeerAdapter } from '.'; -import { log } from './log'; -import { resolve as urlResolve } from 'url'; +import { log, MultipeerAdapter } from '..'; +import { Adapter } from '../internal'; const BUFFER_KEYWORD = 'buffers';