Compare commits

..

2 commits

Author SHA1 Message Date
Jeremy Penner 2b60bacad6 First working body decoding 2023-12-27 15:34:56 -05:00
Jeremy Penner 97c01b459f Start work on re-encoding 2023-12-27 13:46:14 -05:00
18 changed files with 254 additions and 31 deletions

42
avatars.md Normal file
View file

@ -0,0 +1,42 @@
; 0 - number of animation bytes (WRONG? 0x16 in file)
; 1 - disk_face (byte)
; 3 - bits for cels to draw
; 7 - offsets of embedded props for limbs (two bytes each, 6 limbs)
; 19: display_avatar copies _26 bytes_
head_cel_number:
byte 4
frozen_when_stands:
byte 0xff
pattern_for_limb:
byte AVATAR_LEG_LIMB
byte AVATAR_LEG_LIMB
byte AVATAR_ARM_LIMB
byte AVATAR_TORSO_LIMB
byte AVATAR_FACE_LIMB
byte AVATAR_ARM_LIMB
fv_cels: ; order of cels front view
byte 0,1,3,4,2,5
bv_cels:
byte 5,2,4,0,1,3
limbs_affected_by_height:
byte 0,0,1,1,1,1
; limbs are _embedded_ props?? animate.m get_av_prop_address
(A * 2) + 8 - high byte of offset!!
avatars can have up to 16 cels - each frame is _only_ 1 cel, rather than a composite
byte_to_bit lookup table turns it into a bitmask, used by cels_to_draw_2 & cels_to_draw
; embedded limb "prop":
; 0 - number of animation bytes A
; 1 - unk
; 2 - unk
; 3:A+2 - first
; A+3 - unk
; A+4:A+20 - cel offsets (Word)

1
bodies.json Normal file
View file

@ -0,0 +1 @@
["bodies/Avatar.bin","bodies/Drag.bin","bodies/Gunship.bin","bodies/Heli.bin","bodies/MP.bin","bodies/Peng_uppercase.bin","bodies/Spid.bin","bodies/Tank.bin","bodies/Tentacle.bin","bodies/fpants.bin","bodies/nillhead.bin"]

BIN
bodies/Heli.bin Normal file

Binary file not shown.

BIN
bodies/MP.bin Normal file

Binary file not shown.

BIN
bodies/fpants.bin Normal file

Binary file not shown.

BIN
bodies/nillhead.bin Normal file

Binary file not shown.

62
body.html Normal file
View file

@ -0,0 +1,62 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8"/>
<title>Inhabitor - The Habitat Inspector</title>
<style type="text/css">
body {
margin:40px auto;
line-height:1.6;
font-size:18px;
color:#444;
padding:0 10px
}
h1,h2,h3 {
line-height:1.2
}
</style>
<script src="index.js"></script>
</head>
<body>
<h1 id="filename"></h1>
<div id="cels">
<h2>Cels</h1>
</div>
<div id="data">
<h2>Data</h1>
</div>
<div id="errors"></div>
<a href="index.html">Back</a>
<script>
const propFilter = (key, value) => {
if (key != "bitmap" && key != "data" && key != "canvas") {
return value
}
}
const dumpProp = (prop, container) => {
container.appendChild(textNode(JSON.stringify(prop, propFilter, 2), "pre"))
}
const onload = async () => {
const q = new URLSearchParams(window.location.search)
const filename = q.get("f")
document.getElementById("filename").innerText = filename
try {
const body = await decodeBinary(filename, decodeBody)
dumpProp(body, document.getElementById("data"))
if (body.error) {
showError(body.error, filename)
} else {
const celContainer = document.getElementById("cels")
for (const limb of body.limbs) {
showCels(limb, celContainer)
}
}
} catch (e) {
showError(e, filename)
}
}
onload()
</script>
</body>
</html>

View file

@ -48,7 +48,7 @@
const filename = q.get("f") const filename = q.get("f")
document.getElementById("filename").innerText = filename document.getElementById("filename").innerText = filename
try { try {
const prop = await decodeBinary(filename) const prop = await decodeBinary(filename, decodeProp)
dumpProp(prop, document.getElementById("data")) dumpProp(prop, document.getElementById("data"))
if (prop.error) { if (prop.error) {
showError(prop.error, filename) showError(prop.error, filename)

View file

@ -22,10 +22,11 @@
} }
const displayEverything = async () => { const displayEverything = async () => {
await displayList("heads.json", "heads") await displayPropList("heads.json", "heads")
await displayList("props.json", "props") await displayPropList("props.json", "props")
await displayList("misc.json", "misc") await displayPropList("misc.json", "misc")
await displayList("beta.json", "beta") await displayPropList("beta.json", "beta")
await displayBodyList("bodies.json", "bodies")
} }
displayEverything() displayEverything()
@ -48,6 +49,9 @@
Habitat source archive</a> released by the MADE on GitHub. Duplicates have been removed. Habitat source archive</a> released by the MADE on GitHub. Duplicates have been removed.
Some of these objects were never actually included in any released version of Habitat. Some of these objects were never actually included in any released version of Habitat.
</p> </p>
<div id="bodies">
<h3>Bodies</h3>
</div>
<div id="heads"> <div id="heads">
<h3>The Hall Of Heads</h3> <h3>The Hall Of Heads</h3>
<p>From: <a href="https://github.com/Museum-of-Art-and-Digital-Entertainment/habitat/tree/master/aric/mic/Gr/Heads">aric/mic/Gr/Heads</a></p> <p>From: <a href="https://github.com/Museum-of-Art-and-Digital-Entertainment/habitat/tree/master/aric/mic/Gr/Heads">aric/mic/Gr/Heads</a></p>

158
index.js
View file

@ -17,6 +17,9 @@ const makeCanvas = (w, h) => {
} }
const canvasFromBitmap = (bitmap) => { const canvasFromBitmap = (bitmap) => {
if (bitmap.length == 0 || bitmap[0].length == 0) {
return null
}
const h = bitmap.length const h = bitmap.length
const w = bitmap[0].length * 2 const w = bitmap[0].length * 2
const canvas = makeCanvas(w, h) const canvas = makeCanvas(w, h)
@ -103,6 +106,20 @@ const decodeHowHeld = (byte) => {
} }
} }
const encodeHowHeld = (howHeld) => {
if (howHeld == "swing") {
return 0x00
} else if (howHeld == "out") {
return 0x40
} else if (howHeld == "both") {
return 0x80
} else if (howHeld == "at_side") {
return 0xc0
} else {
throw new Error(`Unknown hold "${howHeld}"`)
}
}
const decodeCelType = (byte) => { const decodeCelType = (byte) => {
const typeVal = byte & 0xc0 const typeVal = byte & 0xc0
if (typeVal == 0x00) { if (typeVal == 0x00) {
@ -120,7 +137,25 @@ const decodeCelType = (byte) => {
} }
} }
const encodeCelType = (type) => {
if (type == "bitmap") {
return 0x00
} else if (type == "text") {
return 0x20
} else if (type == "trap") {
return 0x40
} else if (type == "box") {
return 0x80
} else if (type == "circle") {
return 0xc0
} else {
throw new Error(`Unknown cel type "${type}"`)
}
}
const celDecoder = {} const celDecoder = {}
const celEncoder = {}
celDecoder.bitmap = (data, cel) => { celDecoder.bitmap = (data, cel) => {
// bitmap cells are RLE-encoded vertical strips of bytes. Decoding starts from the bottom-left // bitmap cells are RLE-encoded vertical strips of bytes. Decoding starts from the bottom-left
// and proceeds upwards until the top of the bitmap is hit; then then next vertical strip is decoded. // and proceeds upwards until the top of the bitmap is hit; then then next vertical strip is decoded.
@ -355,11 +390,28 @@ const decodeSide = (byte) => {
return "down" return "down"
} }
} }
const encodeSide = (side) => {
if (side == "left") {
return 0x00
} else if (side == "right") {
return 0x01
} else if (side == "up") {
return 0x02
} else if (side == "down") {
return 0x03
} else {
throw new Error(`Unknown side "${side}"`)
}
}
const decodeWalkto = (byte) => { const decodeWalkto = (byte) => {
return { fromSide: decodeSide(byte), offset: signedByte(byte & 0xfc) } return { fromSide: decodeSide(byte), offset: signedByte(byte & 0xfc) }
} }
const encodeWalkto = ({ fromSide, offset }) => {
return encodeSide(fromSide) | (offset & 0xfc)
}
const decodeProp = (data) => { const decodeProp = (data) => {
const prop = { const prop = {
data: data, data: data,
@ -424,6 +476,44 @@ const decodeProp = (data) => {
return prop return prop
} }
const decodeLimb = (data, limb) => {
limb.frames = []
for (let iframe = 0; iframe < data.getUint8(0); iframe ++) {
limb.frames.push(data.getUint8(3 + iframe))
}
const celOffsetsOff = 4 + limb.frames.length
// I don't understand this at all, but it seems to be correct?
const maxCelIndex = Math.max(...limb.frames) + 1
limb.cels = []
for (let icel = 0; icel <= maxCelIndex; icel ++) {
const celOff = data.getUint16(celOffsetsOff + (icel * 2), LE)
limb.cels.push(decodeCel(new DataView(data.buffer, data.byteOffset + celOff)))
}
}
const decodeBody = (data) => {
const body = {
data: data,
headCelNumber: data.getUint8(19),
frozenWhenStands: data.getUint8(20),
frontFacingLimbOrder: [],
backFacingLimbOrder: [],
limbs: []
}
for (let ilimb = 0; ilimb < 6; ilimb ++) {
body.frontFacingLimbOrder.push(data.getUint8(27 + ilimb))
body.backFacingLimbOrder.push(data.getUint8(33 + ilimb))
const limb = {
pattern: data.getUint8(21 + ilimb),
affectedByHeight: data.getUint8(39 + ilimb)
}
const limbOff = data.getUint16(7 + (ilimb * 2), LE)
decodeLimb(new DataView(data.buffer, limbOff), limb)
body.limbs.push(limb)
}
return body
}
const celsFromMask = (prop, celMask) => { const celsFromMask = (prop, celMask) => {
const cels = [] const cels = []
for (let icel = 0; icel < 8; icel ++) { for (let icel = 0; icel < 8; icel ++) {
@ -486,11 +576,19 @@ const textNode = (text, type = "span") => {
return node return node
} }
const wrapLink = (element, href) => {
const link = document.createElement("a")
link.href = href
link.appendChild(element)
return link
}
const linkDetail = (element, filename) => { const linkDetail = (element, filename) => {
const detailLink = document.createElement("a") return wrapLink(element, `detail.html?f=${filename}`)
detailLink.href = `detail.html?f=${filename}` }
detailLink.appendChild(element)
return detailLink const linkBody = (element, filename) => {
return wrapLink(element, `body.html?f=${filename}`)
} }
const createAnimation = (prop, animation) => { const createAnimation = (prop, animation) => {
@ -559,9 +657,9 @@ const showCels = (prop, container) => {
} }
} }
const decodeBinary = async (filename) => { const decodeBinary = async (filename, decoder) => {
try { try {
const prop = decodeProp(await readBinary(filename)) const prop = decoder(await readBinary(filename))
prop.filename = filename prop.filename = filename
return prop return prop
} catch (e) { } catch (e) {
@ -569,11 +667,11 @@ const decodeBinary = async (filename) => {
} }
} }
const showError = (e, filename) => { const showError = (e, filename, link = (x,_) => x) => {
const container = document.getElementById("errors") const container = document.getElementById("errors")
const errNode = document.createElement("p") const errNode = document.createElement("p")
console.error(e) console.error(e)
errNode.appendChild(linkDetail(textNode(filename, "b"), filename)) errNode.appendChild(link(textNode(filename, "b"), filename))
errNode.appendChild(textNode(e.toString(), "p")) errNode.appendChild(textNode(e.toString(), "p"))
if (e.stack) { if (e.stack) {
errNode.appendChild(textNode(e.stack.toString(), "pre")) errNode.appendChild(textNode(e.stack.toString(), "pre"))
@ -581,26 +679,22 @@ const showError = (e, filename) => {
container.appendChild(errNode) container.appendChild(errNode)
} }
const displayFile = async (filename, container) => { const displayFile = async (filename, container, decode, display, link = (x,_) => x) => {
const prop = await decodeBinary(filename) const prop = await decodeBinary(filename, decode)
if (prop.error) { if (prop.error) {
container.parentNode.removeChild(container) container.parentNode.removeChild(container)
showError(prop.error, prop.filename) showError(prop.error, prop.filename, link)
} else { } else {
try { try {
if (prop.animations.length > 0) { display(prop, container)
showAnimations(prop, container)
} else {
showStates(prop, container)
}
} catch (e) { } catch (e) {
container.parentNode.removeChild(container) container.parentNode.removeChild(container)
showError(e, prop.filename) showError(e, prop.filename, link)
} }
} }
} }
const displayList = async (indexFile, containerId) => { const displayList = async (indexFile, containerId, decode, display, link = (x,_) => x) => {
const response = await fetch(indexFile, { cache: "no-cache" }) const response = await fetch(indexFile, { cache: "no-cache" })
const filenames = await response.json() const filenames = await response.json()
const container = document.getElementById(containerId) const container = document.getElementById(containerId)
@ -610,12 +704,32 @@ const displayList = async (indexFile, containerId) => {
fileContainer.style.margin = "2px" fileContainer.style.margin = "2px"
fileContainer.style.padding = "2px" fileContainer.style.padding = "2px"
fileContainer.style.display = "inline-block" fileContainer.style.display = "inline-block"
fileContainer.appendChild(linkDetail(textNode(filename, "div"), filename)) fileContainer.appendChild(link(textNode(filename, "div"), filename))
container.appendChild(fileContainer) container.appendChild(fileContainer)
if (filename != 'heads/fhead.bin') { displayFile(filename, fileContainer, decode, display, link)
displayFile(filename, fileContainer) }
}
const displayProp = (prop, container) => {
if (prop.filename == 'heads/fhead.bin') {
container.appendChild(textNode("CW: Pixel genitals"))
} else if (prop.animations.length > 0) {
showAnimations(prop, container)
} else { } else {
fileContainer.appendChild(textNode("CW: Pixel genitals")) showStates(prop, container)
} }
} }
const displayBody = (body, container) => {
for (const limb of body.limbs) {
showCels(limb, container)
}
}
const displayPropList = async (indexFile, containerId) => {
await displayList(indexFile, containerId, decodeProp, displayProp, linkDetail)
}
const displayBodyList = async (indexFile, containerId) => {
await displayList(indexFile, containerId, decodeBody, displayBody, linkBody)
} }

View file

@ -1 +1 @@
["misc/Avatar.bin","misc/Drag.bin","misc/Gunship.bin","misc/Peng_uppercase.bin","misc/Spid.bin","misc/Tank.bin","misc/Tentacle.bin","misc/angelwing.bin","misc/kenhead201.bin","misc/kenhead202.bin","misc/kenhead203.bin","misc/kenhead205.bin","misc/kenhead206.bin","misc/kenhead207.bin","misc/kenhead208.bin","misc/kenhead209.bin","misc/kenhead210.bin","misc/kenhead211.bin","misc/kenhead212.bin","misc/kenhead213.bin","misc/kenhead214.bin","misc/kenhead215.bin","misc/kenhead216.bin","misc/kenhead217.bin","misc/kenhead218.bin","misc/kenhead219.bin","misc/kenhead220.bin","misc/kenhead221.bin","misc/kenhead222.bin","misc/kenhead223.bin","misc/kenhead224.bin","misc/kenhead225.bin","misc/kenhead226.bin","misc/kenhead227.bin","misc/kenhead228.bin","misc/kenhead229.bin","misc/kenhead230.bin","misc/kenhead231.bin","misc/kenhead232.bin","misc/kenhead233.bin","misc/kenhead234.bin","misc/kenhead235.bin","misc/kenhead236.bin","misc/kenhead237.bin","misc/kenhead238.bin","misc/kenhead239.bin","misc/mouse0.bin","misc/newhab1.bin","misc/newhab10.bin","misc/newhab11.bin","misc/newhab13.bin","misc/newhab14.bin","misc/newhab15.bin","misc/newhab16.bin","misc/newhab17.bin","misc/newhab18.bin","misc/newhab19.bin","misc/newhab20.bin","misc/newhab21.bin","misc/newhab22.bin","misc/newhab23.bin","misc/newhab24.bin","misc/newhab3.bin","misc/newhab5.bin","misc/newhab6.bin","misc/newhab7.bin","misc/newhab8.bin","misc/newhab9.bin","misc/nillhead.bin"] ["misc/angelwing.bin","misc/kenhead201.bin","misc/kenhead202.bin","misc/kenhead203.bin","misc/kenhead205.bin","misc/kenhead206.bin","misc/kenhead207.bin","misc/kenhead208.bin","misc/kenhead209.bin","misc/kenhead210.bin","misc/kenhead211.bin","misc/kenhead212.bin","misc/kenhead213.bin","misc/kenhead214.bin","misc/kenhead215.bin","misc/kenhead216.bin","misc/kenhead217.bin","misc/kenhead218.bin","misc/kenhead219.bin","misc/kenhead220.bin","misc/kenhead221.bin","misc/kenhead222.bin","misc/kenhead223.bin","misc/kenhead224.bin","misc/kenhead225.bin","misc/kenhead226.bin","misc/kenhead227.bin","misc/kenhead228.bin","misc/kenhead229.bin","misc/kenhead230.bin","misc/kenhead231.bin","misc/kenhead232.bin","misc/kenhead233.bin","misc/kenhead234.bin","misc/kenhead235.bin","misc/kenhead236.bin","misc/kenhead237.bin","misc/kenhead238.bin","misc/kenhead239.bin","misc/mouse0.bin","misc/newhab1.bin","misc/newhab10.bin","misc/newhab11.bin","misc/newhab13.bin","misc/newhab14.bin","misc/newhab15.bin","misc/newhab16.bin","misc/newhab17.bin","misc/newhab18.bin","misc/newhab19.bin","misc/newhab20.bin","misc/newhab21.bin","misc/newhab22.bin","misc/newhab23.bin","misc/newhab24.bin","misc/newhab3.bin","misc/newhab5.bin","misc/newhab6.bin","misc/newhab7.bin","misc/newhab8.bin","misc/newhab9.bin","misc/nillhead.bin"]