From 352fb0ddd04fa0b47bb9d614dbbc14f06f36691f Mon Sep 17 00:00:00 2001 From: Jeremy Penner Date: Fri, 22 Dec 2023 14:59:08 -0500 Subject: [PATCH] First working bitmap decoder --- .gitignore | 1 + afro0.bin | Bin 0 -> 227 bytes index.html | 11 +++ index.js | 272 +++++++++++++++++++++++++++++++++++++++++++++++++++ picture1.bin | Bin 0 -> 73 bytes picture2.bin | Bin 0 -> 223 bytes picture3.bin | Bin 0 -> 222 bytes serve | 1 + 8 files changed, 285 insertions(+) create mode 100644 .gitignore create mode 100644 afro0.bin create mode 100644 index.html create mode 100644 index.js create mode 100644 picture1.bin create mode 100644 picture2.bin create mode 100644 picture3.bin create mode 100755 serve 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 0000000000000000000000000000000000000000..4a2c081f6cf243f2f6a14748c6fe39debf8e5881 GIT binary patch literal 227 zcmW;EF;4<99ES0i{;Q?Lpo0SgVKd=i;4AnMT-uo4!bo7a$-N3j<9-v%EtilkUETCc zxU)#2%T2_S;Y!2uB+v3=pI6xTgT0;h@PID>trox-AD|s)#X3?ZCAxW;bQr_aWHj-9 zK36(TjmD*N!}t17RiF=0sC>2nr3P)K9hMff1+PI(4ZhtNaBXIV#pBta5fOilAjt&s zCXpwcz}a~1vn+BRJX;tDm6xN?IM<*X9Her1E;hT}FX;aV^j+pT&Xv~p1>UU1#WZF` NBK$>P+~Eb{`UnIxMVkNs literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..301e3d7f113cd6b1f6179f3d788795d9538fbf99 GIT binary patch literal 73 zcmX@mz`*cP=6?eh1B>u~4h9A-2Ii@=7+7XPF!NNcRSe9b46JF9kqj(pK;|kipDPv2 QUctZ|#=tdg8IW250OHjXEdT%j literal 0 HcmV?d00001 diff --git a/picture2.bin b/picture2.bin new file mode 100644 index 0000000000000000000000000000000000000000..f31e9d93759bf7f9414280c625817e44dbf8ba3b GIT binary patch literal 223 zcmXYrF>V4e6hxn}3C2h%5~ar4YDGbvi@3| zdShp{b!`Qu8tT@gg0Gy54?*)k-iBa8wMax=Bj2bF?@)WkM$bp}Ov0|dcq=I}U8SD^ eolzy1%;eb}j_7he_kV}qo|i{y*UWOyB&R#~yhWG* literal 0 HcmV?d00001 diff --git a/picture3.bin b/picture3.bin new file mode 100644 index 0000000000000000000000000000000000000000..c4d509b13b173436b0b107d21a7351db45c1aef8 GIT binary patch literal 222 zcmYL@K?=e!6ht2>wpoZf*M=`h$l9xTpQH`7RNZ=jUcrUe@e;1O&?FRv&tqm_K7)t1 z{k%48dGOrN7EaA>J^&rH;+D}3X5g#{PLwm^iq2W2ZYP4VB2