commit 352fb0ddd04fa0b47bb9d614dbbc14f06f36691f Author: Jeremy Penner Date: Fri Dec 22 14:59:08 2023 -0500 First working bitmap decoder diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f3ac583 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.bin \ No newline at end of file diff --git a/afro0.bin b/afro0.bin new file mode 100644 index 0000000..4a2c081 Binary files /dev/null and b/afro0.bin differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..1f7fb12 --- /dev/null +++ b/index.html @@ -0,0 +1,11 @@ + + + + + Inhabitor - The Habitat Inspector + + + +
+ + \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..677584a --- /dev/null +++ b/index.js @@ -0,0 +1,272 @@ +const readBinary = async (url) => { + const response = await fetch(url) + if (!response.ok) { + console.log(response) + throw Error(`Failed to download ${url}`) + } + return new DataView(await response.arrayBuffer()) +} + +const decodeHowHeld = (byte) => { + const heldVal = byte & 0xc0 + if (heldVal == 0) { + return "swing" + } else if (heldVal == 0x40) { + return "out" + } else if (heldVal == 0x80) { + return "both" + } else { + return "at_side" + } +} +const LE = true // little-endian + +const decodeCelType = (byte) => { + const typeVal = byte & 0xc0 + if (typeVal == 0x00) { + if ((byte & 0x20) == 0) { + return "bitmap" + } else { + return "text" + } + } else if (typeVal == 0x40) { + return "trap" + } else if (typeVal == 0x80) { + return "box" + } else { + return "circle" + } +} + +const emptyBitmap = (w, h) => { + const bitmap = [] + for (let y = 0; y < h; y ++) { + const scanline = [] + for (let x = 0; x < w; x ++) { + scanline.push(0) + scanline.push(0) + scanline.push(0) + scanline.push(0) + } + bitmap.push(scanline) + } + return bitmap +} + +const celDecoder = {} +celDecoder.bitmap = (data, cel) => { + const bitmap = emptyBitmap(cel.width, cel.height) + let ibmp = 0 + const end = cel.width * cel.height + const putByte = (byte) => { + const x = Math.floor(ibmp / cel.height) * 4 + const y = (cel.height - (ibmp % cel.height)) - 1 + bitmap[y][x] = (byte & 0xc0) >> 6 + bitmap[y][x + 1] = (byte & 0x3c) >> 4 + bitmap[y][x + 2] = (byte & 0x0c) >> 2 + bitmap[y][x + 3] = (byte & 0x03) + ibmp ++ + } + let i = 6 + while (ibmp < end) { + const byte = data.getUint8(i) + i ++ + if (byte == 0) { + const count = data.getUint8(i) + i ++ + if ((count & 0x80) == 0) { + const val = data.getUint8(i) + i ++ + for (let repeat = 0; repeat < count; repeat ++) { + putByte(val) + } + } else { + // transparent run + for (let repeat = 0; repeat < (count & 0x7f); repeat ++) { + putByte(0) + } + } + } else { + putByte(byte) + } + } + cel.bitmap = bitmap +} + +const makeCanvas = (w, h) => { + const canvas = document.createElement("canvas") + canvas.width = w + canvas.height = h + return canvas +} + +const drawBitmap = (bitmap) => { + const h = bitmap.length + const w = bitmap[0].length * 2 + const canvas = makeCanvas(w, h) + const ctx = canvas.getContext("2d") + const img = ctx.createImageData(w, h) + + const putpixel = (x, y, r, g, b, a) => { + const i = (x * 8) + (y * w * 4) + img.data[i] = r + img.data[i + 1] = g + img.data[i + 2] = b + img.data[i + 3] = a + img.data[i + 4] = r + img.data[i + 5] = g + img.data[i + 6] = b + img.data[i + 7] = a + } + + for (let y = 0; y < bitmap.length; y ++) { + const line = bitmap[y] + for (let x = 0; x < line.length; x ++) { + const pixel = line[x] + if (pixel == 0) { // transparent + putpixel(x, y, 0, 0, 0, 0) + } else if (pixel == 1) { // wild + // TODO: patterns + colors + // for now, always blue + putpixel(x, y, 0, 0, 170, 255) + } else if (pixel == 2) { // black + putpixel(x, y, 0, 0, 0, 255) + } else { // skin + // TODO: custom skin colors + putpixel(x, y, 255, 119, 119, 255) + } + } + } + ctx.putImageData(img, 0, 0) + return canvas +} + +const decodeCel = (data, changesColorRam) => { + const cel = { + data: data, + changesColorRam: changesColorRam, + type: decodeCelType(data.getUint8(0)), + wild: (data.getUint8(0) & 0x10) == 0 ? "color" : "pattern", + width: data.getUint8(0) & 0x0f, + height: data.getUint8(1), + xOffset: data.getInt8(2), + yOffset: data.getInt8(3), + xRel: data.getInt8(4), + yRel: data.getInt8(5) + } + if (celDecoder[cel.type]) { + celDecoder[cel.type](data, cel) + } + if (cel.bitmap) { + cel.image = drawBitmap(cel.bitmap).toDataURL() + } + return cel +} + +const decodeFrame = (byte, stateCount) => { + const frameIndex = byte & 0x7f + if (frameIndex > stateCount) { + return null + } + return { state: frameIndex, cycle: (byte & 0x80) != 0 } +} + +const decodeSide = (byte) => { + const side = byte & 0x03 + if (side == 0x00) { + return "left" + } else if (side == 0x01) { + return "right" + } else if (side == 0x02) { + return "up" + } else { + return "down" + } +} + +const signedByte = (byte) => { + if ((byte & 0x80) != 0) { + const complement = (byte ^ 0xff) + 1 + return -complement + } else { + return byte + } +} + +const decodeWalkto = (byte) => { + return { fromSide: decodeSide(byte), offset: signedByte(byte & 0xfc) } +} + +const decodeProp = (data) => { + let prop = { + data: data, + howHeld: decodeHowHeld(data.getUint8(0)), + colorBitmask: data.getUint8(1), + containerXYOff: data.getUint8(3), // TODO: parse this when nonzero + walkto: { left: decodeWalkto(data.getUint8(4)), right: decodeWalkto(data.getUint8(5)), yoff: data.getInt8(6) }, + frames: [], + celmasks: [], + cels: [] + } + const stateCount = (data.getUint8(0) & 0x3f) + 1 + const graphicStateOff = data.getUint8(2) + const celMasksOff = 7 + const celOffsetsOff = celMasksOff + stateCount + + // The prop structure does not directly encode a count for how many cels there are, but each + // "graphic state" is defined by a bitmask marking which cels are present, and we do know how + // many states there are. We can assume that all cels are referenced by at least one state, + // and use that to determine the cel count. + let allCelsMask = 0 + for (let icelmask = 0; icelmask < stateCount; icelmask ++) { + const celmask = data.getUint8(celMasksOff + icelmask) + prop.celmasks.push(celmask) + allCelsMask |= celmask + } + if (allCelsMask != 0x80 && allCelsMask != 0xc0 && allCelsMask != 0xe0 && allCelsMask != 0xf0 && + allCelsMask != 0xf8 && allCelsMask != 0xfc && allCelsMask != 0xfe && allCelsMask != 0xff) { + throw new Error("Inconsistent graphic state cel masks - implies unused cel data") + } + // The prop structure also does not encode a count for how many frames there are, so we simply + // stop parsing once we find one that doesn't make sense. + // We could also potentially assume that this structure always follows the header (or the + // "container" XY array, if one exists), as that seems to be consistently be the case with all + // the props in the Habitat source tree. + for (let frameOff = graphicStateOff; ; frameOff ++) { + const frame = decodeFrame(data.getUint8(frameOff), stateCount) + if (!frame) { + break + } + prop.frames.push(frame) + } + for (let celOffsetOff = celOffsetsOff; allCelsMask != 0; celOffsetOff += 2) { + const icel = prop.cels.length + const celbit = 0x80 >> icel + prop.cels.push(decodeCel(new DataView(data.buffer, data.getUint16(celOffsetOff, LE)), (prop.colorBitmask & celbit) != 0)) + allCelsMask = (allCelsMask << 1) & 0xff + } + return prop +} + +const showCels = (prop) => { + const container = document.getElementById("cels") + for (const cel of prop.cels) { + if (cel.image) { + const img = document.createElement("img") + img.src = cel.image + img.width = cel.width * 4 * 2 * 3 + img.height = cel.height * 3 + img.style.imageRendering = "pixelated" + container.appendChild(img) + } + } +} +const doTheThing = async () => { + const prop = decodeProp(await readBinary("picture1.bin")) + console.log(prop) + showCels(prop) + showCels(decodeProp(await readBinary("picture2.bin"))) + showCels(decodeProp(await readBinary("picture3.bin"))) +} + +doTheThing() \ No newline at end of file diff --git a/picture1.bin b/picture1.bin new file mode 100644 index 0000000..301e3d7 Binary files /dev/null and b/picture1.bin differ diff --git a/picture2.bin b/picture2.bin new file mode 100644 index 0000000..f31e9d9 Binary files /dev/null and b/picture2.bin differ diff --git a/picture3.bin b/picture3.bin new file mode 100644 index 0000000..c4d509b Binary files /dev/null and b/picture3.bin differ diff --git a/serve b/serve new file mode 100755 index 0000000..9541a28 --- /dev/null +++ b/serve @@ -0,0 +1 @@ +npx http-server -o .