msagent.js: Parse character info
msagent.js can now parse the character section of a ACS file fully. This also adds a testbed thing. Later on it will display individual frames and animations for debugging before the full character API is ready. For now, it just has a file input for dumping ACS files into so we can debug reading of the data.
This commit is contained in:
parent
ba3ce91410
commit
2c8793a796
7 changed files with 504 additions and 2 deletions
BIN
msagent.js/res/wordballoon.xcf
Normal file
BIN
msagent.js/res/wordballoon.xcf
Normal file
Binary file not shown.
117
msagent.js/src/buffer.ts
Normal file
117
msagent.js/src/buffer.ts
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
// This is more a utility thing but it will more than likely only ever
|
||||||
|
// be used in the msagent.js code so /shrug
|
||||||
|
|
||||||
|
export enum SeekDir {
|
||||||
|
BEG = 0,
|
||||||
|
CUR = 1,
|
||||||
|
END = 2
|
||||||
|
};
|
||||||
|
|
||||||
|
// A helper over DataView to make it more ergonomic for parsing file data.
|
||||||
|
export class BufferStream {
|
||||||
|
private bufferImpl: Uint8Array;
|
||||||
|
private dataView: DataView;
|
||||||
|
private readPointer: number = 0;
|
||||||
|
|
||||||
|
constructor(buffer: Uint8Array, byteOffset?: number) {
|
||||||
|
this.bufferImpl = buffer;
|
||||||
|
this.dataView = new DataView(this.bufferImpl.buffer, byteOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
seek(where: number, whence: SeekDir) {
|
||||||
|
switch(whence) {
|
||||||
|
case SeekDir.BEG:
|
||||||
|
this.readPointer = where;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SeekDir.CUR:
|
||||||
|
this.readPointer += where;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SeekDir.END:
|
||||||
|
if(where > 0)
|
||||||
|
throw new Error("Cannot use SeekDir.END with where greater than 0");
|
||||||
|
|
||||||
|
this.readPointer = this.bufferImpl.length + whence;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.readPointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
tell() { return this.seek(0, SeekDir.CUR); }
|
||||||
|
|
||||||
|
// common impl function for read*()
|
||||||
|
private readImpl<T>(func: (this: DataView, offset: number, le?: boolean|undefined) => T, size: number, le?: boolean|undefined) {
|
||||||
|
let res = func.call(this.dataView, this.readPointer, le);
|
||||||
|
this.readPointer += size;
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a view of a part of the buffer.
|
||||||
|
// THIS DOES NOT DEEP COPY!
|
||||||
|
subBuffer(len: number) {
|
||||||
|
let oldReadPointer = this.readPointer;
|
||||||
|
let buffer = this.bufferImpl.subarray(oldReadPointer, oldReadPointer + len);
|
||||||
|
this.readPointer += len;
|
||||||
|
return new BufferStream(buffer, oldReadPointer);
|
||||||
|
}
|
||||||
|
|
||||||
|
readS8() { return this.readImpl(DataView.prototype.getInt8, 1); }
|
||||||
|
readU8() { return this.readImpl(DataView.prototype.getUint8, 1); }
|
||||||
|
readS16LE() { return this.readImpl(DataView.prototype.getInt16, 2, true); }
|
||||||
|
readS16BE() { return this.readImpl(DataView.prototype.getInt16, 2, false); }
|
||||||
|
readU16LE() { return this.readImpl(DataView.prototype.getUint16, 2, true); }
|
||||||
|
readU16BE() { return this.readImpl(DataView.prototype.getUint16, 2, false); }
|
||||||
|
readS32LE() { return this.readImpl(DataView.prototype.getInt32, 4, true); }
|
||||||
|
readS32BE() { return this.readImpl(DataView.prototype.getInt32, 4, false); }
|
||||||
|
readU32LE() { return this.readImpl(DataView.prototype.getUint32, 4, true); }
|
||||||
|
readU32BE() { return this.readImpl(DataView.prototype.getUint32, 4, false); }
|
||||||
|
|
||||||
|
// converts easy!
|
||||||
|
readBool() : boolean {
|
||||||
|
let res = this.readU8();
|
||||||
|
return res != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
readString<TChar extends number>(len: number, charReader: (this: BufferStream) => TChar): string {
|
||||||
|
let str = "";
|
||||||
|
|
||||||
|
for(let i = 0; i < len; ++i)
|
||||||
|
str += String.fromCharCode(charReader.call(this));
|
||||||
|
|
||||||
|
// dispose of a nul terminator
|
||||||
|
charReader.call(this);
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
readPascalString(lengthReader: (this: BufferStream) => number = BufferStream.prototype.readU32LE, charReader: (this: BufferStream) => number = BufferStream.prototype.readU16LE) {
|
||||||
|
let len = lengthReader.call(this);
|
||||||
|
if(len == 0)
|
||||||
|
return "";
|
||||||
|
|
||||||
|
return this.readString(len, charReader);
|
||||||
|
}
|
||||||
|
|
||||||
|
readDataChunk(lengthReader: (this: BufferStream) => number = BufferStream.prototype.readU32LE) {
|
||||||
|
let len = lengthReader.call(this);
|
||||||
|
return this.subBuffer(len).raw();
|
||||||
|
}
|
||||||
|
|
||||||
|
// reads a counted list. The length reader is on the other end so you don't need to specify it
|
||||||
|
// (if it's u32)
|
||||||
|
readCountedList<TObject>(objReader: (stream: BufferStream) => TObject, lengthReader: (this: BufferStream) => number = BufferStream.prototype.readU32LE): TObject[] {
|
||||||
|
let len = lengthReader.call(this);
|
||||||
|
let arr: TObject[] = [];
|
||||||
|
|
||||||
|
for(let i = 0; i < len; ++i)
|
||||||
|
arr.push(objReader(this));
|
||||||
|
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
raw() {
|
||||||
|
return this.bufferImpl;
|
||||||
|
}
|
||||||
|
}
|
342
msagent.js/src/character.ts
Normal file
342
msagent.js/src/character.ts
Normal file
|
@ -0,0 +1,342 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function agentCharacterParseACS(buffer: BufferStream) {
|
||||||
|
let magic = buffer.readU32LE();
|
||||||
|
|
||||||
|
if(magic != 0xabcdabc3) {
|
||||||
|
throw new Error("This is not an ACS file.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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));
|
||||||
|
|
||||||
|
// Read the character info in.
|
||||||
|
buffer.seek(characterInfoLocation.offset, SeekDir.BEG);
|
||||||
|
let characterInfo = AcsCharacterInfo.read(buffer);
|
||||||
|
|
||||||
|
console.log(characterInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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<void> {
|
||||||
|
throw new Error("Not implemented yet");
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
import { wordballoonInit } from "./wordballoon.js";
|
import { wordballoonInit } from "./wordballoon.js";
|
||||||
|
|
||||||
export * from "./types.js";
|
export * from "./types.js";
|
||||||
|
export * from "./character.js";
|
||||||
export * from "./sprite.js";
|
export * from "./sprite.js";
|
||||||
export * from "./wordballoon.js";
|
export * from "./wordballoon.js";
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
{
|
{
|
||||||
"name": "@msagent-chat/webapp",
|
"name": "@msagent-chat/webapp",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "parcel build --no-source-maps --dist-dir dist --public-url '.' src/html/index.html",
|
"build": "parcel build --no-source-maps --dist-dir dist --public-url '.' src/html/index.html src/html/testbed.html",
|
||||||
"serve": "parcel src/html/index.html",
|
"serve": "parcel src/html/index.html src/html/testbed.html",
|
||||||
"clean": "run-script-os",
|
"clean": "run-script-os",
|
||||||
"clean:darwin:linux": "rm -rf dist .parcel-cache",
|
"clean:darwin:linux": "rm -rf dist .parcel-cache",
|
||||||
"clean:win32": "rd /s /q dist .parcel-cache"
|
"clean:win32": "rd /s /q dist .parcel-cache"
|
||||||
|
|
21
webapp/src/html/testbed.html
Normal file
21
webapp/src/html/testbed.html
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" prefix="og: https://ogp.me/ns#">
|
||||||
|
<head>
|
||||||
|
<title>MSAgent Chat - testbed</title>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta property="og:title" content="MSAgent Chat" />
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
<meta property="og:site_name" content="Computernewb's Internet Pub Hub" />
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/98.css" />
|
||||||
|
<link type="text/css" rel="stylesheet" href="../css/style.css" />
|
||||||
|
|
||||||
|
<script type="module" src="../ts/testbed.ts"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="field-row-stacked" style="width: 200px">
|
||||||
|
<label for="testbed-input">ACS to test</label>
|
||||||
|
<input id="testbed-input" type="file" accept=".acs" />
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
21
webapp/src/ts/testbed.ts
Normal file
21
webapp/src/ts/testbed.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
// Testbed code
|
||||||
|
// This will go away when it isn't needed
|
||||||
|
|
||||||
|
import * as msagent from "@msagent-chat/msagent.js";
|
||||||
|
|
||||||
|
let input = document.getElementById("testbed-input") as HTMLInputElement;
|
||||||
|
|
||||||
|
input.addEventListener("change", async () => {
|
||||||
|
|
||||||
|
|
||||||
|
let buffer = await input.files![0].arrayBuffer();
|
||||||
|
|
||||||
|
console.log("About to parse character");
|
||||||
|
msagent.parseCharacterTestbed(new Uint8Array(buffer));
|
||||||
|
console.log("parsed character");
|
||||||
|
})
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", async () => {
|
||||||
|
await msagent.agentInit();
|
||||||
|
console.log("msagent initalized!");
|
||||||
|
})
|
Loading…
Reference in a new issue