parse animation data!

also clean up some offsetting stuff a bit, by adding a RAII-like utility that lets the code temporairly offset elsewhere.
This commit is contained in:
Lily Tsuru 2024-07-04 05:13:35 -04:00
parent 3718b508a5
commit 9d88e332f5
7 changed files with 603 additions and 348 deletions

View file

@ -68,7 +68,15 @@ export class BufferStream {
readU32LE() { return this.readImpl(DataView.prototype.getUint32, 4, true); } readU32LE() { return this.readImpl(DataView.prototype.getUint32, 4, true); }
readU32BE() { return this.readImpl(DataView.prototype.getUint32, 4, false); } 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 { readBool() : boolean {
let res = this.readU8(); let res = this.readU8();
return res != 0; return res != 0;
@ -80,7 +88,8 @@ export class BufferStream {
for(let i = 0; i < len; ++i) for(let i = 0; i < len; ++i)
str += String.fromCharCode(charReader.call(this)); 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); charReader.call(this);
return str; return str;
} }
@ -93,9 +102,13 @@ export class BufferStream {
return this.readString(len, charReader); 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); 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 // 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<TObject>(objReader: (stream: BufferStream) => TObject, lengthReader: (this: BufferStream) => number = BufferStream.prototype.readU32LE): TObject[] { readCountedList<TObject>(objReader: (stream: BufferStream) => TObject, lengthReader: (this: BufferStream) => number = BufferStream.prototype.readU32LE): TObject[] {
let len = lengthReader.call(this); let len = lengthReader.call(this);
let arr: TObject[] = []; let arr: TObject[] = [];
if(len == 0)
return arr;
for(let i = 0; i < len; ++i) for(let i = 0; i < len; ++i)
arr.push(objReader(this)); arr.push(objReader(this));

View file

@ -1,30 +1,50 @@
import { BufferStream, SeekDir } from "./buffer.js"; import { BufferStream, SeekDir } from './buffer.js';
import { LOCATION } from "./structs/core.js"; import { LOCATION } from './structs/core.js';
import { AcsCharacterInfo } from "./structs/character.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) { function agentCharacterParseACS(buffer: BufferStream) {
let magic = buffer.readU32LE(); // Make sure the magic is correct for the ACS file.
if (buffer.readU32LE() != 0xabcdabc3) {
if(magic != 0xabcdabc3) { throw new Error('The provided data buffer does not contain valid ACS data.');
throw new Error("This is not an ACS file.");
} }
let acsData = new AcsData();
// Read the rest of the header. // Read the rest of the header.
let characterInfoLocation = LOCATION.read(buffer); let characterInfoLocation = LOCATION.read(buffer);
let animationInfoLocation = LOCATION.read(buffer); let animationInfoLocation = LOCATION.read(buffer);
let imageInfoLocation = LOCATION.read(buffer); let imageInfoLocation = LOCATION.read(buffer);
let audioInfoLocation = 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.withOffset(characterInfoLocation.offset, () => {
buffer.seek(characterInfoLocation.offset, SeekDir.BEG); acsData.characterInfo = AcsCharacterInfo.read(buffer);
let characterInfo = AcsCharacterInfo.read(buffer); });
console.log(characterInfo)
// 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 // For the testbed code only, remove when that gets axed
@ -36,6 +56,6 @@ export function agentParseCharacterTestbed(buffer: Uint8Array) {
// TODO this will be the public API // 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 // 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?) // (which we then mount characters and their wordballoons into?)
export function agentCreateCharacter(data: Uint8Array) : Promise<void> { export function agentCreateCharacter(data: Uint8Array): Promise<void> {
throw new Error("Not implemented yet"); throw new Error('Not implemented yet');
} }

View file

@ -1,4 +1,3 @@
// Please note that the "meaningless" shifts of 0 are to force // Please note that the "meaningless" shifts of 0 are to force
// the value to be a 32-bit integer. Do not remove them. // the value to be a 32-bit integer. Do not remove them.
@ -24,90 +23,83 @@ export function compressDecompress(src: Uint8Array, dest: Uint8Array) {
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 // Make sure the bitstream is valid
if(src.length <= 7 || src[0] != 0) if (src.length <= 7 || src[0] != 0) return 0;
return 0;
for(bitCount = 1; src[src.length - bitCount] == 0xff; bitCount++) { for (bitCount = 1; src[src.length - bitCount] == 0xff; bitCount++) {
if(bitCount > 6) if (bitCount > 6) break;
break;
} }
if(bitCount < 6) if (bitCount < 6) return 0;
return 0;
bitCount = 0; bitCount = 0;
srcPtr += 5; srcPtr += 5;
while((srcPtr < src.length) && (destPtr < dest.length)) { while (srcPtr < src.length && destPtr < dest.length) {
let quad = dv.getUint32(srcPtr - 4, true); let quad = dv.getUint32(srcPtr - 4, true);
if(quad & (1 << LOWORD(bitCount))) { if (quad & (1 << LOWORD(bitCount))) {
srcOffset = 1; srcOffset = 1;
if(quad & (1 << LOWORD(bitCount+1))) { if (quad & (1 << LOWORD(bitCount + 1))) {
if(quad & (1 << LOWORD(bitCount + 2))) { if (quad & (1 << LOWORD(bitCount + 2))) {
if(quad & (1 << LOWORD(bitCount + 3))) { if (quad & (1 << LOWORD(bitCount + 3))) {
quad >>= LOWORD(bitCount + 4); quad >>= LOWORD(bitCount + 4);
quad &= 0x000FFFFF; quad &= 0x000fffff;
// End of compressed bitstream // End of compressed bitstream
if(quad == 0x000FFFFF) if (quad == 0x000fffff) break;
break;
quad += 4673; quad += 4673;
bitCount += 24; bitCount += 24;
srcOffset = 2; srcOffset = 2;
} else { } else {
quad >>= LOWORD(bitCount + 4); quad >>= LOWORD(bitCount + 4);
quad &= 0x0000FFF; quad &= 0x0000fff;
quad += 577; quad += 577;
bitCount += 16; bitCount += 16;
} }
} else { } else {
quad >>= LOWORD(bitCount + 3); quad >>= LOWORD(bitCount + 3);
quad &= 0x000001FF; quad &= 0x000001ff;
quad += 65; quad += 65;
bitCount += 12; bitCount += 12;
} }
} else { } else {
quad >>= LOWORD(bitCount + 2); quad >>= LOWORD(bitCount + 2);
quad &= 0x0000003F; quad &= 0x0000003f;
quad += 1; quad += 1;
bitCount += 8; bitCount += 8;
} }
srcPtr += (bitCount / 8); srcPtr += bitCount / 8;
bitCount &= 7; bitCount &= 7;
let runCount = 0; let runCount = 0;
let runLength = dv.getUint32(srcPtr - 4, true); let runLength = dv.getUint32(srcPtr - 4, true);
while(runLength & (1 << LOWORD(bitCount + runCount))) { while (runLength & (1 << LOWORD(bitCount + runCount))) {
runCount++; runCount++;
if(runCount > 11) if (runCount > 11) break;
break;
} }
runLength >>= LOWORD(bitCount + runCount + 1); runLength >>= LOWORD(bitCount + runCount + 1);
runLength &= (1 << runCount) -1; runLength &= (1 << runCount) - 1;
runLength += 1 << runCount; runLength += 1 << runCount;
runLength += srcOffset; runLength += srcOffset;
bitCount = runCount * 2 + 1; bitCount = runCount * 2 + 1;
if(destPtr + runLength > dest.length) if (destPtr + runLength > dest.length) break;
break;
while(runLength > 0) { while (runLength > 0) {
putb(dest[destPtr - quad]); putb(dest[destPtr - quad]);
runLength--; runLength--;
} }
} else { } else {
// a literal byte // a literal byte
quad >>= LOWORD(bitCount + 1) quad >>= LOWORD(bitCount + 1);
bitCount += 9; bitCount += 9;
putb(LOBYTE(quad)); putb(LOBYTE(quad));
} }

View file

@ -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!

View file

@ -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;
}
}

View file

@ -1,23 +1,23 @@
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 // DA doesn't test individual bits, but for brevity I do
export enum AcsCharacterInfoFlags { export enum AcsCharacterInfoFlags {
// This agent is configured for a given TTS. // This agent is configured for a given TTS.
VoiceOutput = (1 << 5), VoiceOutput = 1 << 5,
// Could be a 2-bit value (where 01 = disable and 10 = enable) // Could be a 2-bit value (where 01 = disable and 10 = enable)
// I wonder why. // I wonder why.
WordBalloonDisabled = (1 << 8), WordBalloonDisabled = 1 << 8,
WordBalloonEnabled = (1 << 9), WordBalloonEnabled = 1 << 9,
// 16-18 are a 3-bit unsigned // 16-18 are a 3-bit unsigned
// value which stores a inner set of // value which stores a inner set of
// bits to control the style of the wordballoon. // bits to control the style of the wordballoon.
StandardAnimationSet = (1 << 20) StandardAnimationSet = 1 << 20
}; }
export class AcsCharacterInfo { export class AcsCharacterInfo {
minorVersion = 0; minorVersion = 0;
@ -38,8 +38,8 @@ export class AcsCharacterInfo {
animSetMajorVer = 0; animSetMajorVer = 0;
animSetMinorVer = 0; animSetMinorVer = 0;
voiceInfo : AcsVoiceInfo | null = null; voiceInfo: AcsVoiceInfo | null = null;
balloonInfo : AcsBalloonInfo | null = null; balloonInfo: AcsBalloonInfo | null = null;
// The color palette. // The color palette.
palette: RGBAColor[] = []; palette: RGBAColor[] = [];
@ -69,14 +69,11 @@ export class AcsCharacterInfo {
info.animSetMajorVer = buffer.readU16LE(); info.animSetMajorVer = buffer.readU16LE();
info.animSetMinorVer = buffer.readU16LE(); info.animSetMinorVer = buffer.readU16LE();
if((info.flags & AcsCharacterInfoFlags.VoiceOutput)) { if (info.flags & AcsCharacterInfoFlags.VoiceOutput) {
info.voiceInfo = AcsVoiceInfo.read(buffer); info.voiceInfo = AcsVoiceInfo.read(buffer);
} }
if( if (info.flags & AcsCharacterInfoFlags.WordBalloonEnabled && !(info.flags & AcsCharacterInfoFlags.WordBalloonDisabled)) {
(info.flags & AcsCharacterInfoFlags.WordBalloonEnabled) &&
!(info.flags & AcsCharacterInfoFlags.WordBalloonDisabled)
) {
info.balloonInfo = AcsBalloonInfo.read(buffer); info.balloonInfo = AcsBalloonInfo.read(buffer);
} }
@ -88,7 +85,7 @@ export class AcsCharacterInfo {
info.palette[info.transparencyColorIndex].a = 0; info.palette[info.transparencyColorIndex].a = 0;
// Tray icon // Tray icon
if(buffer.readBool() == true) { if (buffer.readBool() == true) {
info.trayIcon = AcsTrayIcon.read(buffer); info.trayIcon = AcsTrayIcon.read(buffer);
} }
@ -98,33 +95,30 @@ export class AcsCharacterInfo {
return AcsStateInfo.read(buffer); return AcsStateInfo.read(buffer);
}, BufferStream.prototype.readU16LE); }, BufferStream.prototype.readU16LE);
if(info.localizationInfoListLocation.offset != 0) { if (info.localizationInfoListLocation.offset != 0) {
let lastOffset = buffer.tell(); let lastOffset = buffer.tell();
buffer.seek(info.localizationInfoListLocation.offset, SeekDir.BEG); buffer.seek(info.localizationInfoListLocation.offset, SeekDir.BEG);
info.localizedInfo = buffer.readCountedList(() => { info.localizedInfo = buffer.readCountedList(() => {
return AcsLocalizedInfo.read(buffer); return AcsLocalizedInfo.read(buffer);
}, BufferStream.prototype.readU16LE) }, BufferStream.prototype.readU16LE);
buffer.seek(lastOffset, SeekDir.BEG); buffer.seek(lastOffset, SeekDir.BEG);
} }
return info; return info;
} }
} }
export class AcsVoiceInfoExtraData { export class AcsVoiceInfoExtraData {
langId = 0; langId = 0;
langDialect = ""; langDialect = '';
gender = 0; gender = 0;
age = 0; age = 0;
style = ""; style = '';
static read(buffer: BufferStream) { static read(buffer: BufferStream) {
let info = new AcsVoiceInfoExtraData(); let info = new AcsVoiceInfoExtraData();
@ -140,7 +134,7 @@ export class AcsVoiceInfoExtraData {
return info; return info;
} }
}; }
export class AcsVoiceInfo { export class AcsVoiceInfo {
ttsEngineId = new GUID(); ttsEngineId = new GUID();
@ -161,13 +155,13 @@ export class AcsVoiceInfo {
info.pitch = buffer.readU16LE(); info.pitch = buffer.readU16LE();
// extraData member // extraData member
if(buffer.readBool()) { if (buffer.readBool()) {
info.extraData = AcsVoiceInfoExtraData.read(buffer); info.extraData = AcsVoiceInfoExtraData.read(buffer);
} }
return info; return info;
} }
}; }
export class AcsBalloonInfo { export class AcsBalloonInfo {
nrTextLines = 0; nrTextLines = 0;
@ -177,7 +171,7 @@ export class AcsBalloonInfo {
backColor = new RGBAColor(); backColor = new RGBAColor();
borderColor = new RGBAColor(); borderColor = new RGBAColor();
fontName = ""; fontName = '';
fontHeight = 0; fontHeight = 0;
fontWeight = 0; fontWeight = 0;
@ -204,7 +198,7 @@ export class AcsBalloonInfo {
return info; return info;
} }
}; }
export class AcsTrayIcon { export class AcsTrayIcon {
monoBitmap: Uint8Array | null = null; monoBitmap: Uint8Array | null = null;
@ -219,8 +213,8 @@ export class AcsTrayIcon {
} }
export class AcsStateInfo { export class AcsStateInfo {
stateName = ""; stateName = '';
animations : string[] = []; animations: string[] = [];
static read(buffer: BufferStream) { static read(buffer: BufferStream) {
let info = new AcsStateInfo(); let info = new AcsStateInfo();
@ -236,9 +230,9 @@ export class AcsStateInfo {
export class AcsLocalizedInfo { export class AcsLocalizedInfo {
langId = 0; langId = 0;
charName = ""; charName = '';
charDescription = ""; charDescription = '';
charExtraData = ""; charExtraData = '';
static read(buffer: BufferStream) { static read(buffer: BufferStream) {
let info = new AcsLocalizedInfo(); let info = new AcsLocalizedInfo();

View file

@ -1,4 +1,63 @@
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 { export class GUID {
bytes: number[] = []; bytes: number[] = [];
@ -6,12 +65,13 @@ export class GUID {
static read(buffer: BufferStream) { static read(buffer: BufferStream) {
let guid = new GUID(); let guid = new GUID();
for(var i = 0; i < 16; ++i) for (var i = 0; i < 16; ++i) {
guid.bytes.push(buffer.readU8()); guid.bytes.push(buffer.readU8());
}
return guid; return guid;
} }
}; }
export class LOCATION { export class LOCATION {
offset: number = 0; offset: number = 0;
@ -23,7 +83,7 @@ export class LOCATION {
loc.size = buffer.readU32LE(); loc.size = buffer.readU32LE();
return loc; return loc;
} }
}; }
export class RGBAColor { export class RGBAColor {
r = 0; r = 0;