From 90b6994c2a1133ca506411d59489e43304c0f630 Mon Sep 17 00:00:00 2001 From: Abbodarr <16695760+Abbodarr@users.noreply.github.com> Date: Wed, 29 Sep 2021 16:54:31 -0500 Subject: [PATCH] Added workaround for bug in MRE synchronization layer. --- src/app.ts | 23 ++++++++- src/sync-fix.ts | 132 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 153 insertions(+), 2 deletions(-) create mode 100644 src/sync-fix.ts diff --git a/src/app.ts b/src/app.ts index 07de95c..619f049 100644 --- a/src/app.ts +++ b/src/app.ts @@ -5,6 +5,8 @@ import * as MRE from '@microsoft/mixed-reality-extension-sdk'; +import * as SyncFix from './sync-fix'; + const fetch = require('node-fetch'); const DEBUG = false; @@ -65,6 +67,13 @@ export default class WearAHat { // Options private previewMargin = 1.5; // spacing between preview objects + /* + * Create a UserSyncFix object to work around Altspace MRE library late joiner bugs. + * Parameter is the number if millseconds to wait before attachments are synchronized. All + * users that join within that timeframe with have their synchronizations "batched". + */ + private syncFix: SyncFix.UserSyncFix = new SyncFix.UserSyncFix(5000); + private controls = '../public/defaults.json'; /** @@ -145,6 +154,10 @@ export default class WearAHat { }); this.context.onUserLeft(user => this.userLeft(user)); + + /* Add a userJoined function to let the syncFix object know a new user has joined. */ + this.syncFix.autoSynchronizeAttachments(context); + this.context.onUserJoined(() => this.syncFix.userJoined()); } /** @@ -192,7 +205,7 @@ export default class WearAHat { const hatRecord = this.HatDatabase[hatId]; // Create a clickable button. - var button; +// var button: MRE.Actor; // special scaling and rotation for menu const rotation = hatRecord.menuRotation ? hatRecord.menuRotation : { x: 0, y: 0, z: 0 } @@ -217,7 +230,7 @@ export default class WearAHat { }); // Create an invisible cube with a collider - button = MRE.Actor.CreatePrimitive(this.assets, { + const button = MRE.Actor.CreatePrimitive(this.assets, { definition: { shape: MRE.PrimitiveShape.Box, dimensions: { x: 0.4, y: 0.4, z: 0.4 } // make sure there's a gap @@ -241,6 +254,12 @@ export default class WearAHat { // Set a click handler on the button. // NOTE: button press event fails on MAC button.setBehavior(MRE.ButtonBehavior).onClick(user => this.wearHat(hatId, user.id)); + + this.syncFix.addSyncFunc(() => { + button.setBehavior(MRE.ButtonBehavior).onClick(user => this.wearHat(hatId, user.id)) + }); + + //button.setBehavior(MRE.ButtonBehavior).onButton('pressed', user => this.wearHat(hatId, user.id)); x += this.previewMargin; diff --git a/src/sync-fix.ts b/src/sync-fix.ts new file mode 100644 index 0000000..ac1f43e --- /dev/null +++ b/src/sync-fix.ts @@ -0,0 +1,132 @@ +/* + * Automate fixing late user sync issues with Altspace. + * + * Usage: + * 1. Create an instance of UserSyncFix with the constructor. Pass it the minimum number of milliseconds + * between synchronizations. + * e.g.: syncfix = new UserSyncFix(5000); + * 2. As you create items that need the synchronization fix (such as buttons/button behavior and attached objects), + * call addSyncFunc( ). + * e.g. syncId = syncfix.addSyncFunc(reattachItem); + * For attached objects, the function should detach and reattach them. + * For a button, the function should set the ButtonBehaviors that were already set. + * 3. In the App's userJoined() callback, call UserSyncFix's userJoined() function. + * e.g. syncfix.userJoined() + * 4. As you delete an object (or want to stop synchronizing for some reason), call removeSyncFunc(). + * e.g. syncfix.removeSyncFunc(syncId) + * 'syncId' is the return value from a previous call to addSyncFunc(). + * + */ + +import * as MRE from '@microsoft/mixed-reality-extension-sdk'; + +export class UserSyncFix { + // Map of syncId's to functions to call at each sync time. + private syncFuncs = new Map(); + private syncTimer: Promise; + private _autoSyncNbr: number = null; + + // The next ID to be used when a function is added. + private nextId = 0; + + /** + * Constructor + * @param _minSyncIntervalms Minimum synchronization interval. Users that join in an + * interval with have their synchronization process "batched" + * together to reduce the number of times it needs to happen. + */ + // Constructor. + constructor(private _minSyncIntervalms: number) { + } + + /** + * User should call this form the userJoined event handler to let this sync + * know that a new user has joined. + */ + public userJoined() { + // If a timer hasn't already been created, add it. + if (!this.syncTimer) { + this.syncTimer = new Promise(resolve => setTimeout(resolve, this._minSyncIntervalms)) + .then(() => this._runSyncFuncs()); + } + } + + /** + * runSyncFunc() + * Runs the synchronization functions after a timer has expired. + */ + private _runSyncFuncs() { + // Set the syncTimer to null to prepare for the next user. + this.syncTimer = null; + + // Loop through and execute all the sync functions. + this.syncFuncs.forEach((f, id) => { + f(); + }); + } + + /** + * Control whether to auto-synchronize attachments + * + * @param context The context of the instance. + * @param sync true to synchronize, false to stop synchronizing. + */ + public autoSynchronizeAttachments(context: MRE.Context, sync: boolean = true) { + /* + * If sync is true and we haven't already added the attachment + * sync function, add it. + */ + if (sync && (this._autoSyncNbr === null)) { + this._autoSyncNbr = this.addSyncFunc(() => UserSyncFix._synchronizeAttachments(context)); + } else if (!sync && (this._autoSyncNbr !== null)) { + /* + * Turn off synchronization. + */ + this.removeSyncFunc(this._autoSyncNbr); + this._autoSyncNbr = null; + } + } + + /** + * Synchronize all attachments. + * + * @param context The MRE.Context of the current instance of the MRE. + */ + private static _synchronizeAttachments(context: MRE.Context) { + /* Loop through all the Actors, reattaching any that are attachments. */ + for (const actor of context.actors) { + if (actor.attachment) { + /* Save the attachpoint and userID so we can reattach them. */ + const attachPoint: MRE.AttachPoint = actor.attachment.attachPoint; + const userId: MRE.Guid = actor.attachment.userId; + + /* + * Detatch and reattach the actor. + */ + actor.detach(); + actor.attach(userId, attachPoint); + } + } + } + + /** + * Add a new synchronization function to be called after users join. + * @param f The synchronization function to add to the list to be called. + * @returns An ID that can be used to remove this sync function later. + */ + public addSyncFunc(f: { (): void }): number { + this.syncFuncs.set(this.nextId, f); + + return this.nextId++; + } + + /** + * Remove a sync function from the list. + * @param id The ID returned by a call to addSyncFunc() + */ + public removeSyncFunc(id: number) { + if (this.syncFuncs.has(id)) { + this.syncFuncs.delete(id); + } + } +}