From 95708da8cc3a700006c521a1e61ff9e9ee94bcbb Mon Sep 17 00:00:00 2001 From: modeco80 Date: Tue, 26 Nov 2024 23:37:44 -0500 Subject: [PATCH] msagent.js/web: Implement graceful exit This implementation isn't perfect (sometimes an animation can get stuck playing), but it does work for the most part. (All animation playing logic is async/promise now, which is fine because we only ever used the promise versions) --- msagent.js/web/src/agent.ts | 117 +++++++++++++++++++++++++++--------- webapp/src/ts/client.ts | 2 +- webapp/src/ts/testbed.ts | 4 +- webapp/src/ts/user.ts | 2 +- 4 files changed, 92 insertions(+), 33 deletions(-) diff --git a/msagent.js/web/src/agent.ts b/msagent.js/web/src/agent.ts index d023df2..81f8f8c 100644 --- a/msagent.js/web/src/agent.ts +++ b/msagent.js/web/src/agent.ts @@ -10,44 +10,85 @@ function randint(min: number, max: number) { return Math.floor(Math.random() * (max - min) + min); } +enum AnimState { + Idle = 0, + Cancel = 1, + Playing = 2 +} + +// an externally resolveable promise +class ExternallyResolveablePromise { + private rescb: (t: T) => void; + private rejectcb: (e: Error) => void; + public promise: Promise; + + constructor() { + let rescb; + let rejectcb; + + this.promise = new Promise((res, rej) => { + rescb = res; + rejectcb = rej; + }); + + this.rescb = rescb!; + this.rejectcb = rejectcb!; + } + + resolve(t: T) { + this.rescb(t); + } + + reject(e: Error) { + this.rejectcb(e); + } + + raw() { + return this.promise; + } +} + // animation state (used during animation playback) class AgentAnimationState { char: Agent; anim: AcsAnimation; - private cancelled: boolean = false; - finishCallback: () => void; + private animState: AnimState; + private finishPromise = new ExternallyResolveablePromise(); + frameIndex = 0; - interval = 0; - constructor(char: Agent, anim: AcsAnimation, finishCallback: () => void) { + constructor(char: Agent, anim: AcsAnimation) { this.char = char; this.anim = anim; - this.finishCallback = finishCallback; + this.animState = AnimState.Idle; } // start playing the animation - play() { + async play() { + this.animState = AnimState.Playing; this.nextFrame(); + await this.finishPromise.raw(); + + this.animState = AnimState.Idle; + clearInterval(this.interval); } - cancel() { - this.cancelled = true; - clearTimeout(this.interval); + async cancel() { + this.animState = AnimState.Cancel; + await this.finishPromise.raw(); } nextFrame() { requestAnimationFrame(() => { - if (this.cancelled) return; - // Handle animation branching, if it is required let bi = this.anim.frameInfo[this.frameIndex].branchInfo; if (bi.length != 0) { let biCopy = [...bi]; // This happens more often then you'd think, but this basically handles - // a branch that will always be taken. + // a branch that will always be taken. // // This is often used for looping from my understanding? if (bi.length == 1 && bi[0].branchFrameProbability == 100) { @@ -61,7 +102,7 @@ class AgentAnimationState { // Handles the off chance that there is a branch info list that sums less than 100%. // (Office Logo 'Idle3', Victor has a couple, ...) // - // I'm not entirely sure the correct action in this case but I just + // I'm not entirely sure the correct action in this case but I just // have this do nothing. if (totalProbability != 100) { let nothingBranchItem = new AcsBranchInfo(); @@ -85,10 +126,26 @@ class AgentAnimationState { } } - this.char.drawAnimationFrame(this.anim.frameInfo[this.frameIndex++]); + // Actually draw the requested frame + let fi = this.anim.frameInfo[this.frameIndex]; + + this.char.drawAnimationFrame(fi); + + // If we've requested to cancel the animation and we're in a branch loop, + // then this is how we get out of it. + if (this.animState == AnimState.Cancel) { + if (fi.branchExitFrameIndex != -2) { + this.frameIndex = fi.branchExitFrameIndex; + } else { + this.finishPromise.resolve(); + return; + } + } else { + this.frameIndex += 1; + } if (this.frameIndex >= this.anim.frameInfo.length) { - this.finishCallback(); + this.finishPromise.resolve(); return; } @@ -366,30 +423,32 @@ export class Agent { } // add promise versions later. - playAnimation(index: number, finishCallback: () => void) { + async playAnimation(index: number): Promise { if (this.animState != null) { - this.animState.cancel(); - this.animState = null; + await this.animState.cancel(); } + let animInfo = this.data.animInfo[index]; // Create and start the animation state - this.animState = new AgentAnimationState(this, animInfo.animationData, () => { - this.animState = null; - finishCallback(); - }); - this.animState.play(); + this.animState = new AgentAnimationState(this, animInfo.animationData); + await this.animState.play(); + this.animState = null; } - playAnimationByName(name: string, finishCallback: () => void) { + async playAnimationByName(name: string): Promise { let index = this.data.animInfo.findIndex((n) => n.name == name); - if (index !== -1) this.playAnimation(index, finishCallback); + if (index !== -1) return this.playAnimation(index); + //throw new Error(`Unknown animation \"${name}\"`); + return; } - playAnimationByNamePromise(name: string): Promise { - return new Promise((res, rej) => { - this.playAnimationByName(name, () => res()); - }); + async exitAnimation(): Promise { + if (this.animState != null) { + await this.animState.cancel(); + this.animState = null; + } + // (After this we need to play the restpose animation or whatever exit is applicable) } setUsername(username: string, color: string) { diff --git a/webapp/src/ts/client.ts b/webapp/src/ts/client.ts index b0be591..1b6b32f 100644 --- a/webapp/src/ts/client.ts +++ b/webapp/src/ts/client.ts @@ -397,7 +397,7 @@ export class MSAgentClient { let animMsg = msg as MSAgentAnimationMessage; let user = this.users.find((u) => u.username === animMsg.data.username); if (!user || user.muted) return; - await user.agent.playAnimationByNamePromise(animMsg.data.anim); + await user.agent.playAnimationByName(animMsg.data.anim); await user.doAnim('rest'); break; } diff --git a/webapp/src/ts/testbed.ts b/webapp/src/ts/testbed.ts index 37ad034..e6bbfb3 100644 --- a/webapp/src/ts/testbed.ts +++ b/webapp/src/ts/testbed.ts @@ -22,7 +22,7 @@ input.addEventListener('change', async () => { agent.addToDom(mount); agent.show(); - await agent.playAnimationByNamePromise("Show"); + await agent.playAnimationByName("Show"); console.log('Agent created'); }); @@ -40,7 +40,7 @@ form.addEventListener('submit', async (e) => { agent.addToDom(document.body); agent.show(); - await agent.playAnimationByNamePromise("Show"); + await agent.playAnimationByName("Show"); console.log(`Loaded agent from ${url}`); }); diff --git a/webapp/src/ts/user.ts b/webapp/src/ts/user.ts index d8c2b8f..2faf8d2 100644 --- a/webapp/src/ts/user.ts +++ b/webapp/src/ts/user.ts @@ -20,7 +20,7 @@ export class User { async doAnim(action: string) { // @ts-ignore for (let anim of this.animations[action]) { - await this.agent.playAnimationByNamePromise(anim); + await this.agent.playAnimationByName(anim); } } }