Skip to content

Commit

Permalink
Implement region browsing, rename to "Habitat Inspector"
Browse files Browse the repository at this point in the history
  • Loading branch information
jeremypenner committed Jan 6, 2024
1 parent 6d71b0c commit 9a38c4d
Show file tree
Hide file tree
Showing 10 changed files with 250 additions and 92 deletions.
7 changes: 4 additions & 3 deletions inspector/body.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<html>
<head>
<meta charset="UTF-8"/>
<title>Inhabitor - The Habitat Inspector</title>
<title>Habitat Inspector</title>
<style type="text/css">
body {
margin:40px auto;
Expand All @@ -17,7 +17,7 @@
</style>
</head>
<body>
<h1 id="filename"></h1>
<h1>Avatar - <span id ="filename"></span></h1>
<div id="actions">
<h2>Actions</h2>
</div>
Expand All @@ -31,7 +31,8 @@ <h2>Cels</h1>
<h2>Data</h1>
</div>
<div id="errors"></div>
<a href="index.html">Back</a>
<a href="index.html">Home</a>

<script type="module">
import { decodeBody, choreographyActions } from "./codec.js"
import { docBuilder, decodeBinary, showAll, actionShower, limbAnimationShower, celShower, textNode } from "./show.js"
Expand Down
1 change: 1 addition & 0 deletions inspector/db/contextmap.json

Large diffs are not rendered by default.

40 changes: 40 additions & 0 deletions inspector/db/indexer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import fs from 'node:fs/promises'
import { parseHabitatObject } from "../neohabitat.js"

const buildJsonList = async (directory, jsonList) => {
for (const dirent of (await fs.readdir(directory, { withFileTypes: true }))) {
if (dirent.isFile() && dirent.name.endsWith(".json")) {
jsonList.push(`${directory}/${dirent.name}`)
} else if (dirent.isDirectory()) {
await buildJsonList(`${directory}/${dirent.name}`, jsonList)
}
}
}

const buildContextMap = async (filenames) => {
const contextMap = {}
for (const filename of filenames) {
const data = await fs.readFile(filename, { encoding: "utf-8" })
const objects = parseHabitatObject(data)
if (Array.isArray(objects)) {
for (const obj of objects) {
if (obj.type == "context") {
contextMap[obj.ref] = {
filename: filename.replace(/^\.\//, "db/"),
name: obj.name
}
}
}
}
}
return contextMap
}

const saveContextMap = async () => {
const filenames = []
await buildJsonList(".", filenames)
const contextMap = await buildContextMap(filenames)
await fs.writeFile("contextmap.json", JSON.stringify(contextMap))
}

saveContextMap()
6 changes: 3 additions & 3 deletions inspector/detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<html>
<head>
<meta charset="UTF-8"/>
<title>Inhabitor - The Habitat Inspector</title>
<title>Habitat Inspector</title>
<style type="text/css">
body {
margin:40px auto;
Expand All @@ -17,7 +17,7 @@
</style>
</head>
<body>
<h1 id="filename"></h1>
<h1>Image detail - <span id="filename"></span></h1>
<div id="animations">
<h2>Animations</h2>
</div>
Expand All @@ -31,7 +31,7 @@ <h2>Cels</h1>
<h2>Data</h1>
</div>
<div id="errors"></div>
<a href="index.html">Back</a>
<a href="index.html">Home</a>
<script type="module">
import { decodeProp } from "./codec.js"
import { docBuilder, decodeBinary, showAll, textNode, propAnimationShower, celmaskShower, celShower } from "./show.js"
Expand Down
15 changes: 10 additions & 5 deletions inspector/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<html>
<head>
<meta charset="UTF-8"/>
<title>Inhabitor - The Habitat Inspector</title>
<title>Habitat Inspector</title>
<style type="text/css">
body {
margin:40px auto;
Expand Down Expand Up @@ -34,22 +34,27 @@
</script>
</head>
<body>
<h1>Inhabitor - The Habitat Inspector</h1>
<h1>Habitat Inspector - Art Catalogue</h1>
<p>
You are looking at a collection of object graphics from
<a href="https://frandallfarmer.github.io/neohabitat-doc/docs/">Lucasfilm Games' Habitat</a>. These images are
generated by parsing Habitat's internal binary image / animation format in
JavaScript. The full <a href="https://git.information-superhighway.net/SpindleyQ/inhabitor">
source code is freely available</a>. There are missing features in the rendering
code that means that these images may not be a completely accurate representation
of what is actually seen in-game.
source code is freely available</a>.
</p>
<p>
The Habitat inspector is a work in progress and not all visuals may be fully accurate to
how things appear in-game.
</p>
<p>
The images are <code>.bin</code> files that have been pulled from the
<a href="https://github.com/Museum-of-Art-and-Digital-Entertainment/habitat">
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>
<p>
You may also <a href="region.html">browse all of the regions</a> in Neohabitat.
</p>
<div id="bodies">
<h3>Avatars</h3>
</div>
Expand Down
8 changes: 8 additions & 0 deletions inspector/neohabitat.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,11 @@ export function colorsFromOrientation(orientation) {
return { pattern: colorVal }
}
}

const javaTypeOverrides = {
Teleport: "class_teleport_booth"
}

export const javaTypeToMuddleClass = (type) => {
return javaTypeOverrides[type] ?? `class_${type.toLowerCase()}`
}
3 changes: 3 additions & 0 deletions inspector/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"type": "module"
}
108 changes: 27 additions & 81 deletions inspector/region.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<html>
<head>
<meta charset="UTF-8"/>
<title>Inhabitor - The Habitat Inspector</title>
<title>Habitat Inspector</title>
<style type="text/css">
body {
margin:40px auto;
Expand All @@ -17,100 +17,46 @@
</style>
</head>
<body>
<h1>Inhabitor - The Habitat Inspector</h1>
<h1>Region - <span id="filename"></span></h1>
<div id="region" style="position: relative; width: 960px; height: 384px; overflow: hidden;">
</div>
<div id="debug"></div>
<div>
<h2>Neighboring regions</h2>
<ul id="nav"></ul>
</div>
<div>
<h2>Objects</h2>
<ul id="objects"></ul>
</div>
<div id="errors"></div>
<a href="index.html">Home</a>

<script type="module">
import { decodeProp } from "./codec.js"
import { parse, removeComments } from "./mudparse.js"
import { translateSpace, topLeftCanvasOffset } from "./render.js"
import { docBuilder, decodeBinary, showAll, textNode, propAnimationShower, celmaskShower, celShower } from "./show.js"
import { parseHabitatObject, colorsFromOrientation } from "./neohabitat.js"
import { regionShower } from "./region.js"

window.showErrors = () => {
document.getElementById('errors').style.display = 'block'
}
const debug = (msg, element) => {
const node = textNode(msg, "p")
if (element) {
node.addEventListener("mouseenter", () => { element.style.border = "2px solid red"; element.style.margin = "-2px" })
node.addEventListener("mouseleave", () => { element.style.border = ""; element.style.margin = "" })
}
document.getElementById("debug").appendChild(node)
}
const q = new URLSearchParams(window.location.search)
const filename = q.get("f") ?? "db/new_Downtown/Downtown_3f.json"
const filename = q.get("f") ?? "db/new_Downtown/Downtown_4d.json"

const onload = async () => {
const doc = docBuilder({ errorContainer: document.getElementById("errors")})
const mud = parse(await (await fetch("beta.mud", { cache: "no-cache" })).text())
const objects = parseHabitatObject(await (await fetch(filename, { cache: "no-cache" })).text())
const container = document.getElementById("region")
const sortedObjects = objects
.filter((obj) => obj.type == "item" && obj.mods && obj.mods.length > 0)
.toSorted(((a, b) => {
const ay = a.mods[0].y
const by = b.mods[0].y
const aIsBG = ay < 128
const bIsBG = by < 128
if (aIsBG != bIsBG) {
return aIsBG ? -1 : 1
} else if (aIsBG) {
return ay - by
} else {
return by - ay
}
}))

for (const obj of sortedObjects) {
if (obj.type != "item" || !obj.mods || obj.mods.length == 0) {
continue
const observer = (ev, obj) => {
if (ev == "filename") {
document.getElementById('filename').innerText = obj
} else if (ev == "object" && obj.type == "context" && obj.name) {
document.getElementById('filename').innerText = obj.name
}
const mod = obj.mods[0]
const classname = `class_${mod.type.toLowerCase()}`
const cls = mud.class[classname]
if (!cls) {
doc.showError(`No class named ${classname}`, filename)
continue
}
const style = mod.style ?? 0
if (!cls.image[style]) {
doc.showError(`Invalid style ${mod.style} for ${classname}`, filename)
continue
}
const imageId = cls.image[style].id
const image = mud.image[imageId]
if (!image) {
doc.showError(`${classname} refers to invalid image ${imageId}`, filename)
continue
}
const propFilename = image.filename.replace(/^Images\//, "props/")
const prop = await decodeBinary(propFilename, decodeProp)
if (prop.error) {
doc.showError(prop.error, filename)
continue
}
const colors = colorsFromOrientation(mod.orientation)
const shouldFlip = ((mod.orientation ?? 0) & 0x01) != 0
const middleOrientationBits = (mod.orientation ?? 0) & 0x06
const grState = mod.gr_state ?? 0
const [width, flipOffset] = image.arguments ?? [0,0]
const render = prop.animations.length > 0 ? propAnimationShower(prop, colors)(prop.animations[grState])
: celmaskShower(prop, colors)(prop.celmasks[grState])
const element = render.element
const regionSpace = { minX: 0, minY: 0, maxX: 160 / 4, maxY: 127 }
const objectSpace = translateSpace(render, mod.x / 4, mod.y % 128)
const [x, y] = topLeftCanvasOffset(regionSpace, objectSpace)
element.style.position = "absolute"
element.style.left = `${x * 3}px`
element.style.top = `${y * 3}px`
debug(`${classname}: ${propFilename} ${shouldFlip} w:${width} fo:${flipOffset} o:${middleOrientationBits} [${render.minX}:${render.maxX},${render.minY}:${render.maxY}] @ ${mod.x/4},${mod.y} > ${x},${y}`, element)
container.appendChild(element)
}
// container.appendChild(textNode(JSON.stringify(objects, null, 2), "pre"))
const showRegion = await regionShower(
{ errorContainer: document.getElementById("errors"),
regionContainer: document.getElementById("region"),
objectContainer: document.getElementById("objects"),
navContainer: document.getElementById("nav"),
observer
}
)
await showRegion(filename)
}
onload()
</script>
Expand Down
Loading

0 comments on commit 9a38c4d

Please sign in to comment.