From 9d88e332f551bf16ad8942a663bd8b709c25f32f Mon Sep 17 00:00:00 2001 From: modeco80 Date: Thu, 4 Jul 2024 05:13:35 -0400 Subject: [PATCH] parse animation data! also clean up some offsetting stuff a bit, by adding a RAII-like utility that lets the code temporairly offset elsewhere. --- msagent.js/src/buffer.ts | 23 +- msagent.js/src/character.ts | 62 +++-- msagent.js/src/decompress.ts | 160 ++++++------ msagent.js/src/structs/README.md | 13 + msagent.js/src/structs/animation.ts | 161 ++++++++++++ msagent.js/src/structs/character.ts | 392 ++++++++++++++-------------- msagent.js/src/structs/core.ts | 140 +++++++--- 7 files changed, 603 insertions(+), 348 deletions(-) create mode 100644 msagent.js/src/structs/README.md create mode 100644 msagent.js/src/structs/animation.ts diff --git a/msagent.js/src/buffer.ts b/msagent.js/src/buffer.ts index bf62e9a..49e1e63 100644 --- a/msagent.js/src/buffer.ts +++ b/msagent.js/src/buffer.ts @@ -68,7 +68,15 @@ export class BufferStream { readU32LE() { return this.readImpl(DataView.prototype.getUint32, 4, true); } readU32BE() { return this.readImpl(DataView.prototype.getUint32, 4, false); } - // converts easy! + // Use this for temporary offset modification, e.g: when reading + // a structure *pointed to* inside another structure. + withOffset(where: number, cb: () => void) { + let last = this.tell(); + this.seek(where, SeekDir.BEG); + cb(); + this.seek(last, SeekDir.BEG); + } + readBool() : boolean { let res = this.readU8(); return res != 0; @@ -80,7 +88,8 @@ export class BufferStream { for(let i = 0; i < len; ++i) str += String.fromCharCode(charReader.call(this)); - // dispose of a nul terminator + // dispose of a nul terminator. We don't support other bare Agent formats, + // so we shouldn't need to add the "support" for that. charReader.call(this); return str; } @@ -93,9 +102,13 @@ export class BufferStream { return this.readString(len, charReader); } - readDataChunk(lengthReader: (this: BufferStream) => number = BufferStream.prototype.readU32LE) { + readDataChunkBuffer(lengthReader: (this: BufferStream) => number = BufferStream.prototype.readU32LE) { let len = lengthReader.call(this); - return this.subBuffer(len).raw(); + return this.subBuffer(len); + } + + readDataChunk(lengthReader: (this: BufferStream) => number = BufferStream.prototype.readU32LE) { + return this.readDataChunkBuffer(lengthReader).raw(); } // reads a counted list. The length reader is on the other end so you don't need to specify it @@ -103,6 +116,8 @@ export class BufferStream { readCountedList(objReader: (stream: BufferStream) => TObject, lengthReader: (this: BufferStream) => number = BufferStream.prototype.readU32LE): TObject[] { let len = lengthReader.call(this); let arr: TObject[] = []; + if(len == 0) + return arr; for(let i = 0; i < len; ++i) arr.push(objReader(this)); diff --git a/msagent.js/src/character.ts b/msagent.js/src/character.ts index 08d8a1f..aeb4929 100644 --- a/msagent.js/src/character.ts +++ b/msagent.js/src/character.ts @@ -1,41 +1,61 @@ -import { BufferStream, SeekDir } from "./buffer.js"; +import { BufferStream, SeekDir } from './buffer.js'; -import { LOCATION } from "./structs/core.js"; -import { AcsCharacterInfo } from "./structs/character.js"; +import { LOCATION } from './structs/core.js'; +import { AcsCharacterInfo } from './structs/character.js'; +import { AcsAnimationEntry } from './structs/animation.js'; +// Experiment for storing parsed data +class AcsData { + characterInfo = new AcsCharacterInfo(); + animInfo: AcsAnimationEntry[] = []; +} + +function logOffset(o: number, name: string) { + let n = o >>> 0; + console.log(name, 'offset:', '0x' + n.toString(16)); +} function agentCharacterParseACS(buffer: BufferStream) { - let magic = buffer.readU32LE(); + // Make sure the magic is correct for the ACS file. + if (buffer.readU32LE() != 0xabcdabc3) { + throw new Error('The provided data buffer does not contain valid ACS data.'); + } - if(magic != 0xabcdabc3) { - throw new Error("This is not an ACS file."); - } + let acsData = new AcsData(); - // Read the rest of the header. - let characterInfoLocation = LOCATION.read(buffer); - let animationInfoLocation = LOCATION.read(buffer); - let imageInfoLocation = LOCATION.read(buffer); - let audioInfoLocation = LOCATION.read(buffer); + // Read the rest of the header. + let characterInfoLocation = LOCATION.read(buffer); + let animationInfoLocation = LOCATION.read(buffer); + let imageInfoLocation = LOCATION.read(buffer); + let audioInfoLocation = LOCATION.read(buffer); - console.log(characterInfoLocation.offset.toString(16)); + logOffset(characterInfoLocation.offset, 'character info'); + logOffset(animationInfoLocation.offset, 'animation info'); + logOffset(imageInfoLocation.offset, 'image info'); + logOffset(audioInfoLocation.offset, 'audio info'); - // Read the character info in. - buffer.seek(characterInfoLocation.offset, SeekDir.BEG); - let characterInfo = AcsCharacterInfo.read(buffer); - console.log(characterInfo) + buffer.withOffset(characterInfoLocation.offset, () => { + acsData.characterInfo = AcsCharacterInfo.read(buffer); + }); - // Read animation info + buffer.withOffset(animationInfoLocation.offset, () => { + acsData.animInfo = buffer.readCountedList(() => { + return AcsAnimationEntry.read(buffer); + }); + }); + + console.log(acsData); } // For the testbed code only, remove when that gets axed // (or don't, I'm not your dad) export function agentParseCharacterTestbed(buffer: Uint8Array) { - return agentCharacterParseACS(new BufferStream(buffer)); + return agentCharacterParseACS(new BufferStream(buffer)); } // TODO this will be the public API // Dunno about maintaining canvases. We can pass a div into agentInit and add a characterInit() which recieves it // (which we then mount characters and their wordballoons into?) -export function agentCreateCharacter(data: Uint8Array) : Promise { - throw new Error("Not implemented yet"); +export function agentCreateCharacter(data: Uint8Array): Promise { + throw new Error('Not implemented yet'); } diff --git a/msagent.js/src/decompress.ts b/msagent.js/src/decompress.ts index 2aa6cf9..1e51fd3 100644 --- a/msagent.js/src/decompress.ts +++ b/msagent.js/src/decompress.ts @@ -1,118 +1,110 @@ - // Please note that the "meaningless" shifts of 0 are to force // the value to be a 32-bit integer. Do not remove them. function LOWORD(n: number) { - return (n >>> 0) & 0xffff; + return (n >>> 0) & 0xffff; } function LOBYTE(n: number) { - return (n >>> 0) & 0xff; + return (n >>> 0) & 0xff; } function HIWORD(n: number) { - return (n >>> 16) & 0xffff; + return (n >>> 16) & 0xffff; } // Decompress Agent compressed data. This compression algorithm sucks. // [dest] is to be preallocated to the decompressed data size. export function compressDecompress(src: Uint8Array, dest: Uint8Array) { - let bitCount = 0; - let srcPtr = 0; - let destPtr = 0; - let srcOffset = 0; + let bitCount = 0; + let srcPtr = 0; + let destPtr = 0; + let srcOffset = 0; - let dv = new DataView(src.buffer, src.byteOffset, src.byteLength); + let dv = new DataView(src.buffer, src.byteOffset, src.byteLength); - let putb = (b: number) => dest[destPtr++] = b; + let putb = (b: number) => (dest[destPtr++] = b); - // Make sure the bitstream is valid - if(src.length <= 7 || src[0] != 0) - return 0; + // Make sure the bitstream is valid + if (src.length <= 7 || src[0] != 0) return 0; - for(bitCount = 1; src[src.length - bitCount] == 0xff; bitCount++) { - if(bitCount > 6) - break; - } + for (bitCount = 1; src[src.length - bitCount] == 0xff; bitCount++) { + if (bitCount > 6) break; + } - if(bitCount < 6) - return 0; + if (bitCount < 6) return 0; - bitCount = 0; - srcPtr += 5; + bitCount = 0; + srcPtr += 5; - while((srcPtr < src.length) && (destPtr < dest.length)) { - let quad = dv.getUint32(srcPtr - 4, true); + while (srcPtr < src.length && destPtr < dest.length) { + let quad = dv.getUint32(srcPtr - 4, true); - if(quad & (1 << LOWORD(bitCount))) { - srcOffset = 1; + if (quad & (1 << LOWORD(bitCount))) { + srcOffset = 1; - if(quad & (1 << LOWORD(bitCount+1))) { - if(quad & (1 << LOWORD(bitCount + 2))) { - if(quad & (1 << LOWORD(bitCount + 3))) { - quad >>= LOWORD(bitCount + 4); - quad &= 0x000FFFFF; + if (quad & (1 << LOWORD(bitCount + 1))) { + if (quad & (1 << LOWORD(bitCount + 2))) { + if (quad & (1 << LOWORD(bitCount + 3))) { + quad >>= LOWORD(bitCount + 4); + quad &= 0x000fffff; - // End of compressed bitstream - if(quad == 0x000FFFFF) - break; + // End of compressed bitstream + if (quad == 0x000fffff) break; - quad += 4673; - bitCount += 24; - srcOffset = 2; - } else { - quad >>= LOWORD(bitCount + 4); - quad &= 0x0000FFF; - quad += 577; - bitCount += 16; - } - } else { - quad >>= LOWORD(bitCount + 3); - quad &= 0x000001FF; - quad += 65; - bitCount += 12; - } - } else { - quad >>= LOWORD(bitCount + 2); - quad &= 0x0000003F; - quad += 1; - bitCount += 8; - } + quad += 4673; + bitCount += 24; + srcOffset = 2; + } else { + quad >>= LOWORD(bitCount + 4); + quad &= 0x0000fff; + quad += 577; + bitCount += 16; + } + } else { + quad >>= LOWORD(bitCount + 3); + quad &= 0x000001ff; + quad += 65; + bitCount += 12; + } + } else { + quad >>= LOWORD(bitCount + 2); + quad &= 0x0000003f; + quad += 1; + bitCount += 8; + } - srcPtr += (bitCount / 8); - bitCount &= 7; - let runCount = 0; - let runLength = dv.getUint32(srcPtr - 4, true); + srcPtr += bitCount / 8; + bitCount &= 7; + let runCount = 0; + let runLength = dv.getUint32(srcPtr - 4, true); - while(runLength & (1 << LOWORD(bitCount + runCount))) { - runCount++; + while (runLength & (1 << LOWORD(bitCount + runCount))) { + runCount++; - if(runCount > 11) - break; - } + if (runCount > 11) break; + } - runLength >>= LOWORD(bitCount + runCount + 1); - runLength &= (1 << runCount) -1; - runLength += 1 << runCount; - runLength += srcOffset; - bitCount = runCount * 2 + 1; + runLength >>= LOWORD(bitCount + runCount + 1); + runLength &= (1 << runCount) - 1; + runLength += 1 << runCount; + runLength += srcOffset; + bitCount = runCount * 2 + 1; - if(destPtr + runLength > dest.length) - break; + if (destPtr + runLength > dest.length) break; - while(runLength > 0) { - putb(dest[destPtr - quad]); - runLength--; - } + while (runLength > 0) { + putb(dest[destPtr - quad]); + runLength--; + } + } else { + // a literal byte + quad >>= LOWORD(bitCount + 1); + bitCount += 9; + putb(LOBYTE(quad)); + } - } else { - // a literal byte - quad >>= LOWORD(bitCount + 1) - bitCount += 9; - putb(LOBYTE(quad)); - } - - srcPtr += bitCount / 8; - bitCount &= 7; - } + srcPtr += bitCount / 8; + bitCount &= 7; + } } diff --git a/msagent.js/src/structs/README.md b/msagent.js/src/structs/README.md new file mode 100644 index 0000000..27fd399 --- /dev/null +++ b/msagent.js/src/structs/README.md @@ -0,0 +1,13 @@ +# Structs + +This contains all the structures we read. + +# How to use + +Simple. Given a bufferstream that has been already set up, to read a structure, you just do + +```typescript +let obj = TYPE.read(buffer); +``` + +and this will get you a instance of the read type. Easy as that! diff --git a/msagent.js/src/structs/animation.ts b/msagent.js/src/structs/animation.ts new file mode 100644 index 0000000..e5430a0 --- /dev/null +++ b/msagent.js/src/structs/animation.ts @@ -0,0 +1,161 @@ +import { BufferStream } from '../buffer'; +import { LOCATION, RGNDATA } from './core'; + +export enum AcsAnimationTransitionType { + UseReturn = 0x0, + UseExitBranches = 0x1, + None = 0x2 +} + +export enum AcsAnimationOverlayType { + MouthClosed = 0x0, + MouthOpenWideShape1 = 0x1, + MouthOpenWideShape2 = 0x2, + MouthOpenWideShape3 = 0x3, + MouthOpenWideShape4 = 0x4, + MouthOpenMedium = 0x5, + MouthOpenNarror = 0x6 +} + +export class AcsFrameImage { + imageIndex = 0; + xOffset = 0; + yOffset = 0; + + static read(buffer: BufferStream) { + let img = new AcsFrameImage(); + img.imageIndex = buffer.readU32LE(); + img.xOffset = buffer.readU16LE(); + img.yOffset = buffer.readU16LE(); + return img; + } +} + +export class AcsBranchInfo { + branchFrameIndex = 0; + branchFrameProbability = 0; + + static read(buffer: BufferStream) { + let bi = new AcsBranchInfo(); + bi.branchFrameIndex = buffer.readU16LE(); + bi.branchFrameProbability = buffer.readU16LE(); + return bi; + } +} + +export class AcsOverlayInfo { + overlayType = AcsAnimationOverlayType.MouthClosed; + replacesTopImage = false; + overlayImageIndex = 0; + + xOffset = 0; + yOffset = 0; + + width = 0; + height = 0; + + regionData: RGNDATA | null = null; + + static read(buffer: BufferStream) { + let info = new AcsOverlayInfo(); + info.overlayType = buffer.readU8(); + info.replacesTopImage = buffer.readBool(); + info.overlayImageIndex = buffer.readU16LE(); + + // Some stuff we read but don't use + buffer.readU8(); + let regionDataPresent = buffer.readBool(); + + info.xOffset = buffer.readS16LE(); + info.yOffset = buffer.readS16LE(); + + info.width = buffer.readU16LE(); + info.height = buffer.readU16LE(); + + if (regionDataPresent) { + let regionDataBuffer = buffer.readDataChunkBuffer(); + info.regionData = RGNDATA.read(regionDataBuffer); + } + + return info; + } +} + +export class AcsAnimationFrameInfo { + // list type u16 + images: AcsFrameImage[] = []; + + // Currently unused, but this is the sound to play when this frame is played. + // This is used for sound effects. We could play them pretty easily, since + // the audio data is just WAV (and browsers support that fine). + // -1 means no sound should be played. + soundIndex = 0; + + frameDuration = 0; // The duration of the frame in (1/100)th seconds. + nextFrame = 0; // -2 = animation has ended (although, I imagine this could be detected in better ways!) + + branchInfo: AcsBranchInfo[] = []; + overlayInfo: AcsOverlayInfo[] = []; + + static read(buffer: BufferStream) { + let info = new AcsAnimationFrameInfo(); + + info.images = buffer.readCountedList(() => { + return AcsFrameImage.read(buffer); + }, BufferStream.prototype.readU16LE); + + info.soundIndex = buffer.readS16LE(); + info.frameDuration = buffer.readU16LE(); + info.nextFrame = buffer.readS16LE(); + + info.branchInfo = buffer.readCountedList(() => { + return AcsBranchInfo.read(buffer); + }, BufferStream.prototype.readU8); + + info.overlayInfo = buffer.readCountedList(() => { + return AcsOverlayInfo.read(buffer); + }, BufferStream.prototype.readU8); + + return info; + } +} + +export class AcsAnimation { + name = ''; + transitionType = AcsAnimationTransitionType.UseReturn; + returnName = ''; + frameInfo: AcsAnimationFrameInfo[] = []; + + static read(buffer: BufferStream) { + let anim = new AcsAnimation(); + + anim.name = buffer.readPascalString(); + anim.transitionType = buffer.readU8(); + anim.returnName = buffer.readPascalString(); + + anim.frameInfo = buffer.readCountedList(() => { + return AcsAnimationFrameInfo.read(buffer); + }, BufferStream.prototype.readU16LE); + + return anim; + } +} + +export class AcsAnimationEntry { + name = ''; + animationData = new AcsAnimation(); + + static read(buffer: BufferStream) { + let data = new AcsAnimationEntry(); + data.name = buffer.readPascalString(); + + // This is part of the in-file data, but for simplicity + // we read the data here and discard + let animDataLoc = LOCATION.read(buffer); + + buffer.withOffset(animDataLoc.offset, () => { + data.animationData = AcsAnimation.read(buffer); + }); + return data; + } +} diff --git a/msagent.js/src/structs/character.ts b/msagent.js/src/structs/character.ts index dbc6b89..daa24dc 100644 --- a/msagent.js/src/structs/character.ts +++ b/msagent.js/src/structs/character.ts @@ -1,253 +1,247 @@ -import { BufferStream, SeekDir } from "../buffer.js"; +import { BufferStream, SeekDir } from '../buffer.js'; -import { GUID, LOCATION, RGBAColor } from "./core.js"; +import { GUID, LOCATION, RGBAColor } from './core.js'; // DA doesn't test individual bits, but for brevity I do export enum AcsCharacterInfoFlags { - // This agent is configured for a given TTS. - VoiceOutput = (1 << 5), - - // Could be a 2-bit value (where 01 = disable and 10 = enable) - // I wonder why. - WordBalloonDisabled = (1 << 8), - WordBalloonEnabled = (1 << 9), - - // 16-18 are a 3-bit unsigned - // value which stores a inner set of - // bits to control the style of the wordballoon. - - StandardAnimationSet = (1 << 20) -}; + // This agent is configured for a given TTS. + VoiceOutput = 1 << 5, -export class AcsCharacterInfo { - minorVersion = 0; - majorVersion = 0; - - localizationInfoListLocation = new LOCATION(); - - guid = new GUID(); - - charWidth = 0; - charHeight = 0; - - // Color index in the palette for the transparent color - transparencyColorIndex = 0; - - flags = 0; - - animSetMajorVer = 0; - animSetMinorVer = 0; - - voiceInfo : AcsVoiceInfo | null = null; - balloonInfo : AcsBalloonInfo | null = null; - - // The color palette. - palette: RGBAColor[] = []; - - trayIcon: AcsTrayIcon | null = null; - - stateInfo: AcsStateInfo[] = []; - - localizedInfo: AcsLocalizedInfo[] = []; - - static read(buffer: BufferStream) { - let info = new AcsCharacterInfo(); - - info.minorVersion = buffer.readU16LE(); - info.majorVersion = buffer.readU16LE(); - - info.localizationInfoListLocation = LOCATION.read(buffer); - info.guid = GUID.read(buffer); - - info.charWidth = buffer.readU16LE(); - info.charHeight = buffer.readU16LE(); - - info.transparencyColorIndex = buffer.readU8(); - - info.flags = buffer.readU32LE(); - - info.animSetMajorVer = buffer.readU16LE(); - info.animSetMinorVer = buffer.readU16LE(); - - if((info.flags & AcsCharacterInfoFlags.VoiceOutput)) { - info.voiceInfo = AcsVoiceInfo.read(buffer); - } - - if( - (info.flags & AcsCharacterInfoFlags.WordBalloonEnabled) && - !(info.flags & AcsCharacterInfoFlags.WordBalloonDisabled) - ) { - info.balloonInfo = AcsBalloonInfo.read(buffer); - } - - info.palette = buffer.readCountedList(() => { - return RGBAColor.read(buffer); - }); - - // Set transparency for the magic transparency color - info.palette[info.transparencyColorIndex].a = 0; - - // Tray icon - if(buffer.readBool() == true) { - info.trayIcon = AcsTrayIcon.read(buffer); - } - - // this makes me wish typescript had sensible generics - // so this could be encoded in a type, like c++ or rust lol - info.stateInfo = buffer.readCountedList(() => { - return AcsStateInfo.read(buffer); - }, BufferStream.prototype.readU16LE); - - if(info.localizationInfoListLocation.offset != 0) { - let lastOffset = buffer.tell(); - - buffer.seek(info.localizationInfoListLocation.offset, SeekDir.BEG); - - info.localizedInfo = buffer.readCountedList(() => { - return AcsLocalizedInfo.read(buffer); - }, BufferStream.prototype.readU16LE) - - buffer.seek(lastOffset, SeekDir.BEG); - } - - return info; - } + // Could be a 2-bit value (where 01 = disable and 10 = enable) + // I wonder why. + WordBalloonDisabled = 1 << 8, + WordBalloonEnabled = 1 << 9, + // 16-18 are a 3-bit unsigned + // value which stores a inner set of + // bits to control the style of the wordballoon. + StandardAnimationSet = 1 << 20 } +export class AcsCharacterInfo { + minorVersion = 0; + majorVersion = 0; + + localizationInfoListLocation = new LOCATION(); + + guid = new GUID(); + + charWidth = 0; + charHeight = 0; + + // Color index in the palette for the transparent color + transparencyColorIndex = 0; + + flags = 0; + + animSetMajorVer = 0; + animSetMinorVer = 0; + + voiceInfo: AcsVoiceInfo | null = null; + balloonInfo: AcsBalloonInfo | null = null; + + // The color palette. + palette: RGBAColor[] = []; + + trayIcon: AcsTrayIcon | null = null; + + stateInfo: AcsStateInfo[] = []; + + localizedInfo: AcsLocalizedInfo[] = []; + + static read(buffer: BufferStream) { + let info = new AcsCharacterInfo(); + + info.minorVersion = buffer.readU16LE(); + info.majorVersion = buffer.readU16LE(); + + info.localizationInfoListLocation = LOCATION.read(buffer); + info.guid = GUID.read(buffer); + + info.charWidth = buffer.readU16LE(); + info.charHeight = buffer.readU16LE(); + + info.transparencyColorIndex = buffer.readU8(); + + info.flags = buffer.readU32LE(); + + info.animSetMajorVer = buffer.readU16LE(); + info.animSetMinorVer = buffer.readU16LE(); + + if (info.flags & AcsCharacterInfoFlags.VoiceOutput) { + info.voiceInfo = AcsVoiceInfo.read(buffer); + } + + if (info.flags & AcsCharacterInfoFlags.WordBalloonEnabled && !(info.flags & AcsCharacterInfoFlags.WordBalloonDisabled)) { + info.balloonInfo = AcsBalloonInfo.read(buffer); + } + + info.palette = buffer.readCountedList(() => { + return RGBAColor.read(buffer); + }); + + // Set transparency for the magic transparency color + info.palette[info.transparencyColorIndex].a = 0; + + // Tray icon + if (buffer.readBool() == true) { + info.trayIcon = AcsTrayIcon.read(buffer); + } + + // this makes me wish typescript had sensible generics + // so this could be encoded in a type, like c++ or rust lol + info.stateInfo = buffer.readCountedList(() => { + return AcsStateInfo.read(buffer); + }, BufferStream.prototype.readU16LE); + + if (info.localizationInfoListLocation.offset != 0) { + let lastOffset = buffer.tell(); + + buffer.seek(info.localizationInfoListLocation.offset, SeekDir.BEG); + + info.localizedInfo = buffer.readCountedList(() => { + return AcsLocalizedInfo.read(buffer); + }, BufferStream.prototype.readU16LE); + + buffer.seek(lastOffset, SeekDir.BEG); + } + + return info; + } +} export class AcsVoiceInfoExtraData { - langId = 0; - langDialect = ""; + langId = 0; + langDialect = ''; - gender = 0; - age = 0; + gender = 0; + age = 0; - style = ""; + style = ''; - static read(buffer: BufferStream) { - let info = new AcsVoiceInfoExtraData(); + static read(buffer: BufferStream) { + let info = new AcsVoiceInfoExtraData(); - info.langId = buffer.readU16LE(); + info.langId = buffer.readU16LE(); - info.langDialect = buffer.readPascalString(); + info.langDialect = buffer.readPascalString(); - info.gender = buffer.readU16LE(); - info.age = buffer.readU16LE(); + info.gender = buffer.readU16LE(); + info.age = buffer.readU16LE(); - info.style = buffer.readPascalString(); + info.style = buffer.readPascalString(); - return info; - } -}; + return info; + } +} export class AcsVoiceInfo { - ttsEngineId = new GUID(); - ttsModeId = new GUID(); + ttsEngineId = new GUID(); + ttsModeId = new GUID(); - speed = 0; - pitch = 0; + speed = 0; + pitch = 0; - extraData: AcsVoiceInfoExtraData | null = null; + extraData: AcsVoiceInfoExtraData | null = null; - static read(buffer: BufferStream) { - let info = new AcsVoiceInfo(); + static read(buffer: BufferStream) { + let info = new AcsVoiceInfo(); - info.ttsEngineId = GUID.read(buffer); - info.ttsModeId = GUID.read(buffer); + info.ttsEngineId = GUID.read(buffer); + info.ttsModeId = GUID.read(buffer); - info.speed = buffer.readU32LE(); - info.pitch = buffer.readU16LE(); + info.speed = buffer.readU32LE(); + info.pitch = buffer.readU16LE(); - // extraData member - if(buffer.readBool()) { - info.extraData = AcsVoiceInfoExtraData.read(buffer); - } - - return info; - } -}; + // extraData member + if (buffer.readBool()) { + info.extraData = AcsVoiceInfoExtraData.read(buffer); + } + + return info; + } +} export class AcsBalloonInfo { - nrTextLines = 0; - charsPerLine = 0; + nrTextLines = 0; + charsPerLine = 0; - foreColor = new RGBAColor(); - backColor = new RGBAColor(); - borderColor = new RGBAColor(); + foreColor = new RGBAColor(); + backColor = new RGBAColor(); + borderColor = new RGBAColor(); - fontName = ""; + fontName = ''; - fontHeight = 0; - fontWeight = 0; + fontHeight = 0; + fontWeight = 0; - italic = false; - unkFlag = false; + italic = false; + unkFlag = false; - static read(buffer: BufferStream) { - let info = new AcsBalloonInfo(); + static read(buffer: BufferStream) { + let info = new AcsBalloonInfo(); - info.nrTextLines = buffer.readU8(); - info.charsPerLine = buffer.readU8(); + info.nrTextLines = buffer.readU8(); + info.charsPerLine = buffer.readU8(); - info.foreColor = RGBAColor.read(buffer); - info.backColor = RGBAColor.read(buffer); - info.borderColor = RGBAColor.read(buffer); + info.foreColor = RGBAColor.read(buffer); + info.backColor = RGBAColor.read(buffer); + info.borderColor = RGBAColor.read(buffer); - info.fontName = buffer.readPascalString(); - info.fontHeight = buffer.readS32LE(); - info.fontWeight = buffer.readS32LE(); + info.fontName = buffer.readPascalString(); + info.fontHeight = buffer.readS32LE(); + info.fontWeight = buffer.readS32LE(); - info.italic = buffer.readBool(); - info.unkFlag = buffer.readBool(); - - return info; - } -}; + info.italic = buffer.readBool(); + info.unkFlag = buffer.readBool(); + + return info; + } +} export class AcsTrayIcon { - monoBitmap: Uint8Array | null = null; - colorBitmap: Uint8Array | null = null; + monoBitmap: Uint8Array | null = null; + colorBitmap: Uint8Array | null = null; - static read(buffer: BufferStream) { - let icon = new AcsTrayIcon(); - icon.monoBitmap = buffer.readDataChunk(BufferStream.prototype.readU32LE); - icon.colorBitmap = buffer.readDataChunk(BufferStream.prototype.readU32LE); - return icon; - } + static read(buffer: BufferStream) { + let icon = new AcsTrayIcon(); + icon.monoBitmap = buffer.readDataChunk(BufferStream.prototype.readU32LE); + icon.colorBitmap = buffer.readDataChunk(BufferStream.prototype.readU32LE); + return icon; + } } export class AcsStateInfo { - stateName = ""; - animations : string[] = []; + stateName = ''; + animations: string[] = []; - static read(buffer: BufferStream) { - let info = new AcsStateInfo(); + static read(buffer: BufferStream) { + let info = new AcsStateInfo(); - info.stateName = buffer.readPascalString(); - info.animations = buffer.readCountedList(() => { - return buffer.readPascalString(); - }, BufferStream.prototype.readU16LE); + info.stateName = buffer.readPascalString(); + info.animations = buffer.readCountedList(() => { + return buffer.readPascalString(); + }, BufferStream.prototype.readU16LE); - return info; - } + return info; + } } export class AcsLocalizedInfo { - langId = 0; - charName = ""; - charDescription = ""; - charExtraData = ""; + langId = 0; + charName = ''; + charDescription = ''; + charExtraData = ''; - static read(buffer: BufferStream) { - let info = new AcsLocalizedInfo(); + static read(buffer: BufferStream) { + let info = new AcsLocalizedInfo(); - info.langId = buffer.readU16LE(); - info.charName = buffer.readPascalString(); - info.charDescription = buffer.readPascalString(); - info.charExtraData = buffer.readPascalString(); + info.langId = buffer.readU16LE(); + info.charName = buffer.readPascalString(); + info.charDescription = buffer.readPascalString(); + info.charExtraData = buffer.readPascalString(); - return info; - } + return info; + } } diff --git a/msagent.js/src/structs/core.ts b/msagent.js/src/structs/core.ts index 9f59869..13cc504 100644 --- a/msagent.js/src/structs/core.ts +++ b/msagent.js/src/structs/core.ts @@ -1,56 +1,116 @@ -import { BufferStream, SeekDir } from "../buffer.js"; +import { BufferStream, SeekDir } from '../buffer.js'; + +// Win32 Rect +export class RECT { + left = 0; + top = 0; + right = 0; + bottom = 0; + + static read(buffer: BufferStream) { + let rect = new RECT(); + rect.left = buffer.readU32LE(); + rect.top = buffer.readU32LE(); + rect.right = buffer.readU32LE(); + rect.bottom = buffer.readU32LE(); + return rect; + } +} + +export class RGNDATAHEADER { + size = 0x20; // I think? + type = 1; + count = 0; + rgnSize = 0; + bound = new RECT(); + + static read(buffer: BufferStream) { + let hdr = new RGNDATAHEADER(); + + hdr.size = buffer.readU32LE(); + //if(hdr.size != 0x20) + // throw new Error("Invalid RGNDATAHEADER!"); + + hdr.type = buffer.readU32LE(); + if (hdr.type != 1) throw new Error('Invalid RGNDATAHEADER type!'); + + hdr.count = buffer.readU32LE(); + hdr.rgnSize = buffer.readU32LE(); + + hdr.bound = RECT.read(buffer); + + return hdr; + } +} + +export class RGNDATA { + header = new RGNDATAHEADER(); + rects: RECT[] = []; + + static read(buffer: BufferStream) { + let region = new RGNDATA(); + region.header = RGNDATAHEADER.read(buffer); + + for (let i = 0; i < region.header.count; ++i) { + region.rects.push(RECT.read(buffer)); + } + + return region; + } +} export class GUID { - bytes: number[] = []; + bytes: number[] = []; - static read(buffer: BufferStream) { - let guid = new GUID(); + static read(buffer: BufferStream) { + let guid = new GUID(); - for(var i = 0; i < 16; ++i) - guid.bytes.push(buffer.readU8()); + for (var i = 0; i < 16; ++i) { + guid.bytes.push(buffer.readU8()); + } - return guid; - } -}; + return guid; + } +} export class LOCATION { - offset: number = 0; - size: number = 0; + offset: number = 0; + size: number = 0; - static read(buffer: BufferStream) { - let loc = new LOCATION(); - loc.offset = buffer.readU32LE(); - loc.size = buffer.readU32LE(); - return loc; - } -}; + static read(buffer: BufferStream) { + let loc = new LOCATION(); + loc.offset = buffer.readU32LE(); + loc.size = buffer.readU32LE(); + return loc; + } +} export class RGBAColor { - r = 0; - g = 0; - b = 0; - a = 0; + r = 0; + g = 0; + b = 0; + a = 0; - // Does what it says on the tin, converts to RGBA - to_rgba(): number { - return (this.r << 24) | (this.g << 16) | (this.b << 8) | this.a; - } + // Does what it says on the tin, converts to RGBA + to_rgba(): number { + return (this.r << 24) | (this.g << 16) | (this.b << 8) | this.a; + } - static from_gdi_rgbquad(val: number) { - let quad = new RGBAColor(); + static from_gdi_rgbquad(val: number) { + let quad = new RGBAColor(); - // Extract individual RGB values from the RGBQUAD - // We ignore the last 8 bits because it is always left - // as 0x00 or if uncleared, just random garbage. - quad.r = (val & 0xff000000) >> 24; - quad.g = (val & 0x00ff0000) >> 16; - quad.b = (val & 0x0000ff00) >> 8; - quad.a = 255; + // Extract individual RGB values from the RGBQUAD + // We ignore the last 8 bits because it is always left + // as 0x00 or if uncleared, just random garbage. + quad.r = (val & 0xff000000) >> 24; + quad.g = (val & 0x00ff0000) >> 16; + quad.b = (val & 0x0000ff00) >> 8; + quad.a = 255; - return quad; - } + return quad; + } - static read(buffer: BufferStream) { - return RGBAColor.from_gdi_rgbquad(buffer.readU32LE()); - } + static read(buffer: BufferStream) { + return RGBAColor.from_gdi_rgbquad(buffer.readU32LE()); + } }