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")
document.getElementById("filename").innerText = filename
try {
const prop = await decodeBinary(filename)
const prop = await decodeBinary(filename, decodeProp)
dumpProp(prop, document.getElementById("data"))
if (prop.error) {
showError(prop.error, filename)

View file

@ -22,10 +22,11 @@
}
const displayEverything = async () => {
await displayList("heads.json", "heads")
await displayList("props.json", "props")
await displayList("misc.json", "misc")
await displayList("beta.json", "beta")
await displayPropList("heads.json", "heads")
await displayPropList("props.json", "props")
await displayPropList("misc.json", "misc")
await displayPropList("beta.json", "beta")
await displayBodyList("bodies.json", "bodies")
}
displayEverything()
@ -48,6 +49,9 @@
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.
</p>
<div id="bodies">
<h3>Bodies</h3>
</div>
<div id="heads">
<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>

162
index.js
View file

@ -17,6 +17,9 @@ const makeCanvas = (w, h) => {
}
const canvasFromBitmap = (bitmap) => {
if (bitmap.length == 0 || bitmap[0].length == 0) {
return null
}
const h = bitmap.length
const w = bitmap[0].length * 2
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 typeVal = byte & 0xc0
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 celEncoder = {}
celDecoder.bitmap = (data, cel) => {
// 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.
@ -355,11 +390,28 @@ const decodeSide = (byte) => {
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) => {
return { fromSide: decodeSide(byte), offset: signedByte(byte & 0xfc) }
}
const encodeWalkto = ({ fromSide, offset }) => {
return encodeSide(fromSide) | (offset & 0xfc)
}
const decodeProp = (data) => {
const prop = {
data: data,
@ -424,6 +476,44 @@ const decodeProp = (data) => {
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 cels = []
for (let icel = 0; icel < 8; icel ++) {
@ -486,11 +576,19 @@ const textNode = (text, type = "span") => {
return node
}
const wrapLink = (element, href) => {
const link = document.createElement("a")
link.href = href
link.appendChild(element)
return link
}
const linkDetail = (element, filename) => {
const detailLink = document.createElement("a")
detailLink.href = `detail.html?f=${filename}`
detailLink.appendChild(element)
return detailLink
return wrapLink(element, `detail.html?f=${filename}`)
}
const linkBody = (element, filename) => {
return wrapLink(element, `body.html?f=${filename}`)
}
const createAnimation = (prop, animation) => {
@ -559,9 +657,9 @@ const showCels = (prop, container) => {
}
}
const decodeBinary = async (filename) => {
const decodeBinary = async (filename, decoder) => {
try {
const prop = decodeProp(await readBinary(filename))
const prop = decoder(await readBinary(filename))
prop.filename = filename
return prop
} 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 errNode = document.createElement("p")
console.error(e)
errNode.appendChild(linkDetail(textNode(filename, "b"), filename))
errNode.appendChild(link(textNode(filename, "b"), filename))
errNode.appendChild(textNode(e.toString(), "p"))
if (e.stack) {
errNode.appendChild(textNode(e.stack.toString(), "pre"))
@ -581,26 +679,22 @@ const showError = (e, filename) => {
container.appendChild(errNode)
}
const displayFile = async (filename, container) => {
const prop = await decodeBinary(filename)
const displayFile = async (filename, container, decode, display, link = (x,_) => x) => {
const prop = await decodeBinary(filename, decode)
if (prop.error) {
container.parentNode.removeChild(container)
showError(prop.error, prop.filename)
showError(prop.error, prop.filename, link)
} else {
try {
if (prop.animations.length > 0) {
showAnimations(prop, container)
} else {
showStates(prop, container)
}
display(prop, container)
} catch (e) {
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 filenames = await response.json()
const container = document.getElementById(containerId)
@ -610,12 +704,32 @@ const displayList = async (indexFile, containerId) => {
fileContainer.style.margin = "2px"
fileContainer.style.padding = "2px"
fileContainer.style.display = "inline-block"
fileContainer.appendChild(linkDetail(textNode(filename, "div"), filename))
fileContainer.appendChild(link(textNode(filename, "div"), filename))
container.appendChild(fileContainer)
if (filename != 'heads/fhead.bin') {
displayFile(filename, fileContainer)
} else {
fileContainer.appendChild(textNode("CW: Pixel genitals"))
}
displayFile(filename, fileContainer, decode, display, link)
}
}
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 {
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"]