diff --git a/msagent.js/src/character.ts b/msagent.js/src/character.ts index 5892555..08d8a1f 100644 --- a/msagent.js/src/character.ts +++ b/msagent.js/src/character.ts @@ -1,309 +1,7 @@ import { BufferStream, SeekDir } from "./buffer.js"; -class GUID { - bytes: number[] = []; - - static read(buffer: BufferStream) { - let guid = new GUID(); - - for(var i = 0; i < 16; ++i) - guid.bytes.push(buffer.readU8()); - - return guid; - } -}; - -class LOCATION { - offset: number = 0; - size: number = 0; - - static read(buffer: BufferStream) { - let loc = new LOCATION(); - loc.offset = buffer.readU32LE(); - loc.size = buffer.readU32LE(); - return loc; - } -}; - -class RGBAColor { - r = 0; - g = 0; - b = 0; - a = 0; - - // does what it says on the tin - to_rgba(): number { - return (this.r << 24) | (this.g << 16) | (this.b << 8) | this.a; - } - - static from_gdi_rgbquad(val: number, transparent: boolean = false) { - 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; - - if(transparent) - quad.a = 0; - else - quad.a = 255; - - return quad; - } - - static read(buffer: BufferStream, transparent: boolean = false) { - return RGBAColor.from_gdi_rgbquad(buffer.readU32LE(), transparent); - } -} - -// DA doesn't test individual bits, but for brevity I do -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) -}; - -class AcsVoiceInfoExtraData { - langId = 0; - langDialect = ""; - - gender = 0; - age = 0; - - style = ""; - - static read(buffer: BufferStream) { - let info = new AcsVoiceInfoExtraData(); - - info.langId = buffer.readU16LE(); - - info.langDialect = buffer.readPascalString(); - - info.gender = buffer.readU16LE(); - info.age = buffer.readU16LE(); - - info.style = buffer.readPascalString(); - - return info; - } -}; - -class AcsVoiceInfo { - ttsEngineId = new GUID(); - ttsModeId = new GUID(); - - speed = 0; - pitch = 0; - - extraData: AcsVoiceInfoExtraData | null = null; - - static read(buffer: BufferStream) { - let info = new AcsVoiceInfo(); - - info.ttsEngineId = GUID.read(buffer); - info.ttsModeId = GUID.read(buffer); - - info.speed = buffer.readU32LE(); - info.pitch = buffer.readU16LE(); - - // extraData member - if(buffer.readBool()) { - info.extraData = AcsVoiceInfoExtraData.read(buffer); - } - - return info; - } -}; - -class AcsBalloonInfo { - nrTextLines = 0; - charsPerLine = 0; - - foreColor = new RGBAColor(); - backColor = new RGBAColor(); - borderColor = new RGBAColor(); - - fontName = ""; - - fontHeight = 0; - fontWeight = 0; - - italic = false; - unkFlag = false; - - static read(buffer: BufferStream) { - let info = new AcsBalloonInfo(); - - info.nrTextLines = buffer.readU8(); - info.charsPerLine = buffer.readU8(); - - 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.italic = buffer.readBool(); - info.unkFlag = buffer.readBool(); - - return info; - } -}; - -class AcsTrayIcon { - 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; - } -} - -class AcsStateInfo { - stateName = ""; - animations : string[] = []; - - static read(buffer: BufferStream) { - let info = new AcsStateInfo(); - - info.stateName = buffer.readPascalString(); - info.animations = buffer.readCountedList(() => { - return buffer.readPascalString(); - }, BufferStream.prototype.readU16LE); - - return info; - } -} - -class AcsLocalizedInfo { - langId = 0; - charName = ""; - charDescription = ""; - charExtraData = ""; - - static read(buffer: BufferStream) { - let info = new AcsLocalizedInfo(); - - info.langId = buffer.readU16LE(); - info.charName = buffer.readPascalString(); - info.charDescription = buffer.readPascalString(); - info.charExtraData = buffer.readPascalString(); - - return info; - } -} - -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); - }); - - // Tray icon - if(buffer.readBool() == true) { - info.trayIcon = AcsTrayIcon.read(buffer); - } - - // this makes me wish type 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; - } - - -} +import { LOCATION } from "./structs/core.js"; +import { AcsCharacterInfo } from "./structs/character.js"; function agentCharacterParseACS(buffer: BufferStream) { @@ -324,8 +22,9 @@ function agentCharacterParseACS(buffer: BufferStream) { // Read the character info in. buffer.seek(characterInfoLocation.offset, SeekDir.BEG); let characterInfo = AcsCharacterInfo.read(buffer); - console.log(characterInfo) + + // Read animation info } // For the testbed code only, remove when that gets axed diff --git a/msagent.js/src/decompress.ts b/msagent.js/src/decompress.ts new file mode 100644 index 0000000..2aa6cf9 --- /dev/null +++ b/msagent.js/src/decompress.ts @@ -0,0 +1,118 @@ + +// 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; +} + +function LOBYTE(n: number) { + return (n >>> 0) & 0xff; +} + +function HIWORD(n: number) { + 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 dv = new DataView(src.buffer, src.byteOffset, src.byteLength); + + let putb = (b: number) => dest[destPtr++] = b; + + // 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; + } + + if(bitCount < 6) + return 0; + + bitCount = 0; + srcPtr += 5; + + 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+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; + + 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); + + while(runLength & (1 << LOWORD(bitCount + runCount))) { + runCount++; + + if(runCount > 11) + break; + } + + runLength >>= LOWORD(bitCount + runCount + 1); + runLength &= (1 << runCount) -1; + runLength += 1 << runCount; + runLength += srcOffset; + bitCount = runCount * 2 + 1; + + if(destPtr + runLength > dest.length) + break; + + while(runLength > 0) { + putb(dest[destPtr - quad]); + runLength--; + } + + } else { + // a literal byte + quad >>= LOWORD(bitCount + 1) + bitCount += 9; + putb(LOBYTE(quad)); + } + + srcPtr += bitCount / 8; + bitCount &= 7; + } +} diff --git a/msagent.js/src/index.ts b/msagent.js/src/index.ts index c9209e0..a1cba3b 100644 --- a/msagent.js/src/index.ts +++ b/msagent.js/src/index.ts @@ -2,6 +2,7 @@ import { wordballoonInit } from "./wordballoon.js"; export * from "./types.js"; export * from "./character.js"; +export * from "./decompress.js"; export * from "./sprite.js"; export * from "./wordballoon.js"; diff --git a/msagent.js/src/structs/character.ts b/msagent.js/src/structs/character.ts new file mode 100644 index 0000000..dbc6b89 --- /dev/null +++ b/msagent.js/src/structs/character.ts @@ -0,0 +1,253 @@ +import { BufferStream, SeekDir } from "../buffer.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) +}; + +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 = ""; + + gender = 0; + age = 0; + + style = ""; + + static read(buffer: BufferStream) { + let info = new AcsVoiceInfoExtraData(); + + info.langId = buffer.readU16LE(); + + info.langDialect = buffer.readPascalString(); + + info.gender = buffer.readU16LE(); + info.age = buffer.readU16LE(); + + info.style = buffer.readPascalString(); + + return info; + } +}; + +export class AcsVoiceInfo { + ttsEngineId = new GUID(); + ttsModeId = new GUID(); + + speed = 0; + pitch = 0; + + extraData: AcsVoiceInfoExtraData | null = null; + + static read(buffer: BufferStream) { + let info = new AcsVoiceInfo(); + + info.ttsEngineId = GUID.read(buffer); + info.ttsModeId = GUID.read(buffer); + + info.speed = buffer.readU32LE(); + info.pitch = buffer.readU16LE(); + + // extraData member + if(buffer.readBool()) { + info.extraData = AcsVoiceInfoExtraData.read(buffer); + } + + return info; + } +}; + +export class AcsBalloonInfo { + nrTextLines = 0; + charsPerLine = 0; + + foreColor = new RGBAColor(); + backColor = new RGBAColor(); + borderColor = new RGBAColor(); + + fontName = ""; + + fontHeight = 0; + fontWeight = 0; + + italic = false; + unkFlag = false; + + static read(buffer: BufferStream) { + let info = new AcsBalloonInfo(); + + info.nrTextLines = buffer.readU8(); + info.charsPerLine = buffer.readU8(); + + 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.italic = buffer.readBool(); + info.unkFlag = buffer.readBool(); + + return info; + } +}; + +export class AcsTrayIcon { + 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; + } +} + +export class AcsStateInfo { + stateName = ""; + animations : string[] = []; + + static read(buffer: BufferStream) { + let info = new AcsStateInfo(); + + info.stateName = buffer.readPascalString(); + info.animations = buffer.readCountedList(() => { + return buffer.readPascalString(); + }, BufferStream.prototype.readU16LE); + + return info; + } +} + +export class AcsLocalizedInfo { + langId = 0; + charName = ""; + charDescription = ""; + charExtraData = ""; + + static read(buffer: BufferStream) { + let info = new AcsLocalizedInfo(); + + info.langId = buffer.readU16LE(); + info.charName = buffer.readPascalString(); + info.charDescription = buffer.readPascalString(); + info.charExtraData = buffer.readPascalString(); + + return info; + } +} diff --git a/msagent.js/src/structs/core.ts b/msagent.js/src/structs/core.ts new file mode 100644 index 0000000..9f59869 --- /dev/null +++ b/msagent.js/src/structs/core.ts @@ -0,0 +1,56 @@ +import { BufferStream, SeekDir } from "../buffer.js"; + +export class GUID { + bytes: number[] = []; + + static read(buffer: BufferStream) { + let guid = new GUID(); + + for(var i = 0; i < 16; ++i) + guid.bytes.push(buffer.readU8()); + + return guid; + } +}; + +export class LOCATION { + offset: number = 0; + size: number = 0; + + 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; + + // 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(); + + // 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; + } + + static read(buffer: BufferStream) { + return RGBAColor.from_gdi_rgbquad(buffer.readU32LE()); + } +}