Compare commits

...

2 commits

Author SHA1 Message Date
d9faea4c1c msagent.js/core: Clean up C++/WASM decompression code
mostly just removing C-style casts and replacing the win32 LO* macros with constexpr functions, because they suck

(also, add a gitignore scoped to msagent.js to ignore the obj/ of building the WASM decompression code)
2024-11-26 23:57:23 -05:00
95708da8cc 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)
2024-11-26 23:37:44 -05:00
7 changed files with 124 additions and 57 deletions

1
msagent.js/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
core/obj

View file

@ -2,17 +2,25 @@
// Integer types from WASI-libc // Integer types from WASI-libc
using usize = unsigned int; using usize = unsigned int;
using u32 = unsigned int; using u32 = unsigned int;
using u16 = unsigned short;
using u8 = unsigned char; using u8 = unsigned char;
#define LOWORD(x) (x & 0xffff) template<class T>
#define LOBYTE(x) (x & 0xff) constexpr auto LowOrderShortOf(T item) {
return static_cast<u16>(item & 0xffff);
}
#define PUBLIC __attribute__((visibility("default"))) extern "C" template<class T>
constexpr auto LowOrderByteOf(T item) {
return static_cast<u8>(item & 0xff);
}
PUBLIC usize agentDecompressWASM(const void* pSrcData, usize pSrcSize, void* pTrgData, usize pTrgSize) { #define PUBLIC extern "C" __attribute__((visibility("default")))
const u8* lSrcPtr = (const u8*)pSrcData;
PUBLIC usize agentDecompressWASM(const u8* pSrcData, usize pSrcSize, u8* pTrgData, usize pTrgSize) {
const u8* lSrcPtr = pSrcData;
const u8* lSrcEnd = lSrcPtr + pSrcSize; const u8* lSrcEnd = lSrcPtr + pSrcSize;
u8* lTrgPtr = (u8*)pTrgData; u8* lTrgPtr = pTrgData;
u8* lTrgEnd = lTrgPtr + pTrgSize; u8* lTrgEnd = lTrgPtr + pTrgSize;
u32 lSrcQuad; u32 lSrcQuad;
u8 lTrgByte; u8 lTrgByte;
@ -38,15 +46,15 @@ PUBLIC usize agentDecompressWASM(const void* pSrcData, usize pSrcSize, void* pTr
lSrcPtr += 5; lSrcPtr += 5;
while((lSrcPtr < lSrcEnd) && (lTrgPtr < lTrgEnd)) { while((lSrcPtr < lSrcEnd) && (lTrgPtr < lTrgEnd)) {
lSrcQuad = *(const u32*)(lSrcPtr - sizeof(u32)); lSrcQuad = *reinterpret_cast<const u32*>(lSrcPtr - sizeof(u32));
if(lSrcQuad & (1 << LOWORD(lBitCount))) { if(lSrcQuad & (1 << LowOrderShortOf(lBitCount))) {
lSrcOffset = 1; lSrcOffset = 1;
if(lSrcQuad & (1 << LOWORD(lBitCount + 1))) { if(lSrcQuad & (1 << LowOrderShortOf(lBitCount + 1))) {
if(lSrcQuad & (1 << LOWORD(lBitCount + 2))) { if(lSrcQuad & (1 << LowOrderShortOf(lBitCount + 2))) {
if(lSrcQuad & (1 << LOWORD(lBitCount + 3))) { if(lSrcQuad & (1 << LowOrderShortOf(lBitCount + 3))) {
lSrcQuad >>= LOWORD(lBitCount + 4); lSrcQuad >>= LowOrderShortOf(lBitCount + 4);
lSrcQuad &= 0x000FFFFF; lSrcQuad &= 0x000FFFFF;
if(lSrcQuad == 0x000FFFFF) { if(lSrcQuad == 0x000FFFFF) {
break; break;
@ -56,19 +64,19 @@ PUBLIC usize agentDecompressWASM(const void* pSrcData, usize pSrcSize, void* pTr
lSrcOffset = 2; lSrcOffset = 2;
} else { } else {
lSrcQuad >>= LOWORD(lBitCount + 4); lSrcQuad >>= LowOrderShortOf(lBitCount + 4);
lSrcQuad &= 0x00000FFF; lSrcQuad &= 0x00000FFF;
lSrcQuad += 577; lSrcQuad += 577;
lBitCount += 16; lBitCount += 16;
} }
} else { } else {
lSrcQuad >>= LOWORD(lBitCount + 3); lSrcQuad >>= LowOrderShortOf(lBitCount + 3);
lSrcQuad &= 0x000001FF; lSrcQuad &= 0x000001FF;
lSrcQuad += 65; lSrcQuad += 65;
lBitCount += 12; lBitCount += 12;
} }
} else { } else {
lSrcQuad >>= LOWORD(lBitCount + 2); lSrcQuad >>= LowOrderShortOf(lBitCount + 2);
lSrcQuad &= 0x0000003F; lSrcQuad &= 0x0000003F;
lSrcQuad += 1; lSrcQuad += 1;
lBitCount += 8; lBitCount += 8;
@ -76,16 +84,16 @@ PUBLIC usize agentDecompressWASM(const void* pSrcData, usize pSrcSize, void* pTr
lSrcPtr += (lBitCount / 8); lSrcPtr += (lBitCount / 8);
lBitCount &= 7; lBitCount &= 7;
lRunLgth = *(const u32*)(lSrcPtr - sizeof(u32)); lRunLgth = *reinterpret_cast<const u32*>(lSrcPtr - sizeof(u32));
lRunCount = 0; lRunCount = 0;
while(lRunLgth & (1 << LOWORD(lBitCount + lRunCount))) { while(lRunLgth & (1 << LowOrderShortOf(lBitCount + lRunCount))) {
lRunCount++; lRunCount++;
if(lRunCount > 11) { if(lRunCount > 11) {
break; break;
} }
} }
lRunLgth >>= LOWORD(lBitCount + lRunCount + 1); lRunLgth >>= LowOrderShortOf(lBitCount + lRunCount + 1);
lRunLgth &= (1 << lRunCount) - 1; lRunLgth &= (1 << lRunCount) - 1;
lRunLgth += 1 << lRunCount; lRunLgth += 1 << lRunCount;
lRunLgth += lSrcOffset; lRunLgth += lSrcOffset;
@ -103,10 +111,10 @@ PUBLIC usize agentDecompressWASM(const void* pSrcData, usize pSrcSize, void* pTr
lRunLgth--; lRunLgth--;
} }
} else { } else {
lSrcQuad >>= LOWORD(lBitCount + 1); lSrcQuad >>= LowOrderShortOf(lBitCount + 1);
lBitCount += 9; lBitCount += 9;
lTrgByte = LOBYTE(lSrcQuad); lTrgByte = LowOrderByteOf(lSrcQuad);
*(lTrgPtr++) = lTrgByte; *(lTrgPtr++) = lTrgByte;
} }
@ -114,5 +122,5 @@ PUBLIC usize agentDecompressWASM(const void* pSrcData, usize pSrcSize, void* pTr
lBitCount &= 7; lBitCount &= 7;
} }
return (usize)(lTrgPtr - (u8*)pTrgData); return static_cast<usize>(lTrgPtr - pTrgData);
} }

View file

@ -28,8 +28,7 @@ function compressWASMGetMemory(): WebAssembly.Memory {
// Decompress Agent compressed data. This compression algorithm sucks. // Decompress Agent compressed data. This compression algorithm sucks.
// [dest] is to be preallocated to the decompressed data size. // [dest] is to be preallocated to the decompressed data size.
export function compressDecompress(src: Uint8Array, dest: Uint8Array) { export function compressDecompress(src: Uint8Array, dest: Uint8Array) {
// Grow the WASM heap if needed. Funnily enough, this code is never hit in most // Grow the WASM heap if needed.
// ACSes, so IDK if it's even needed
compressWasm.growHeapTo(src.length + dest.length); compressWasm.growHeapTo(src.length + dest.length);
let memory = compressWASMGetMemory(); let memory = compressWASMGetMemory();
@ -41,7 +40,7 @@ export function compressDecompress(src: Uint8Array, dest: Uint8Array) {
// Call the WASM compression routine // Call the WASM compression routine
let nrBytesDecompressed = compressWasmGetExports().agentDecompressWASM(0, src.length, src.length, dest.length); let nrBytesDecompressed = compressWasmGetExports().agentDecompressWASM(0, src.length, src.length, dest.length);
if (nrBytesDecompressed != dest.length) throw new Error(`decompression failed: ${nrBytesDecompressed} != ${dest.length}`); if (nrBytesDecompressed != dest.length) throw new Error(`Decompression failed: Output ${nrBytesDecompressed} != expected ${dest.length}`);
// The uncompressed data is located at memory[src.length..dest.length]. // The uncompressed data is located at memory[src.length..dest.length].
// Copy it into the destination buffer. // Copy it into the destination buffer.

View file

@ -10,44 +10,85 @@ function randint(min: number, max: number) {
return Math.floor(Math.random() * (max - min) + min); return Math.floor(Math.random() * (max - min) + min);
} }
enum AnimState {
Idle = 0,
Cancel = 1,
Playing = 2
}
// an externally resolveable promise
class ExternallyResolveablePromise<T> {
private rescb: (t: T) => void;
private rejectcb: (e: Error) => void;
public promise: Promise<T>;
constructor() {
let rescb;
let rejectcb;
this.promise = new Promise<T>((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) // animation state (used during animation playback)
class AgentAnimationState { class AgentAnimationState {
char: Agent; char: Agent;
anim: AcsAnimation; anim: AcsAnimation;
private cancelled: boolean = false;
finishCallback: () => void; private animState: AnimState;
private finishPromise = new ExternallyResolveablePromise<void>();
frameIndex = 0; frameIndex = 0;
interval = 0; interval = 0;
constructor(char: Agent, anim: AcsAnimation, finishCallback: () => void) { constructor(char: Agent, anim: AcsAnimation) {
this.char = char; this.char = char;
this.anim = anim; this.anim = anim;
this.finishCallback = finishCallback; this.animState = AnimState.Idle;
} }
// start playing the animation // start playing the animation
play() { async play() {
this.animState = AnimState.Playing;
this.nextFrame(); this.nextFrame();
await this.finishPromise.raw();
this.animState = AnimState.Idle;
clearInterval(this.interval);
} }
cancel() { async cancel() {
this.cancelled = true; this.animState = AnimState.Cancel;
clearTimeout(this.interval); await this.finishPromise.raw();
} }
nextFrame() { nextFrame() {
requestAnimationFrame(() => { requestAnimationFrame(() => {
if (this.cancelled) return;
// Handle animation branching, if it is required // Handle animation branching, if it is required
let bi = this.anim.frameInfo[this.frameIndex].branchInfo; let bi = this.anim.frameInfo[this.frameIndex].branchInfo;
if (bi.length != 0) { if (bi.length != 0) {
let biCopy = [...bi]; let biCopy = [...bi];
// This happens more often then you'd think, but this basically handles // 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? // This is often used for looping from my understanding?
if (bi.length == 1 && bi[0].branchFrameProbability == 100) { 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%. // Handles the off chance that there is a branch info list that sums less than 100%.
// (Office Logo 'Idle3', Victor has a couple, ...) // (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. // have this do nothing.
if (totalProbability != 100) { if (totalProbability != 100) {
let nothingBranchItem = new AcsBranchInfo(); 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) { if (this.frameIndex >= this.anim.frameInfo.length) {
this.finishCallback(); this.finishPromise.resolve();
return; return;
} }
@ -366,30 +423,32 @@ export class Agent {
} }
// add promise versions later. // add promise versions later.
playAnimation(index: number, finishCallback: () => void) { async playAnimation(index: number): Promise<void> {
if (this.animState != null) { if (this.animState != null) {
this.animState.cancel(); await this.animState.cancel();
this.animState = null;
} }
let animInfo = this.data.animInfo[index]; let animInfo = this.data.animInfo[index];
// Create and start the animation state // Create and start the animation state
this.animState = new AgentAnimationState(this, animInfo.animationData, () => { this.animState = new AgentAnimationState(this, animInfo.animationData);
this.animState = null; await this.animState.play();
finishCallback(); this.animState = null;
});
this.animState.play();
} }
playAnimationByName(name: string, finishCallback: () => void) { async playAnimationByName(name: string): Promise<void> {
let index = this.data.animInfo.findIndex((n) => n.name == name); 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<void> { async exitAnimation(): Promise<void> {
return new Promise((res, rej) => { if (this.animState != null) {
this.playAnimationByName(name, () => res()); 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) { setUsername(username: string, color: string) {

View file

@ -397,7 +397,7 @@ export class MSAgentClient {
let animMsg = msg as MSAgentAnimationMessage; let animMsg = msg as MSAgentAnimationMessage;
let user = this.users.find((u) => u.username === animMsg.data.username); let user = this.users.find((u) => u.username === animMsg.data.username);
if (!user || user.muted) return; if (!user || user.muted) return;
await user.agent.playAnimationByNamePromise(animMsg.data.anim); await user.agent.playAnimationByName(animMsg.data.anim);
await user.doAnim('rest'); await user.doAnim('rest');
break; break;
} }

View file

@ -22,7 +22,7 @@ input.addEventListener('change', async () => {
agent.addToDom(mount); agent.addToDom(mount);
agent.show(); agent.show();
await agent.playAnimationByNamePromise("Show"); await agent.playAnimationByName("Show");
console.log('Agent created'); console.log('Agent created');
}); });
@ -40,7 +40,7 @@ form.addEventListener('submit', async (e) => {
agent.addToDom(document.body); agent.addToDom(document.body);
agent.show(); agent.show();
await agent.playAnimationByNamePromise("Show"); await agent.playAnimationByName("Show");
console.log(`Loaded agent from ${url}`); console.log(`Loaded agent from ${url}`);
}); });

View file

@ -20,7 +20,7 @@ export class User {
async doAnim(action: string) { async doAnim(action: string) {
// @ts-ignore // @ts-ignore
for (let anim of this.animations[action]) { for (let anim of this.animations[action]) {
await this.agent.playAnimationByNamePromise(anim); await this.agent.playAnimationByName(anim);
} }
} }
} }