From 43920bf57711d4cbad3b750db8435c0ecc074d11 Mon Sep 17 00:00:00 2001 From: Jeremy Penner Date: Sun, 21 Jan 2024 01:39:09 -0500 Subject: [PATCH] First attempt at container support (x positioning is still wrong) --- inspector/codec.js | 26 ++++++-- inspector/navigate.html | 11 +++- inspector/region.js | 135 +++++++++++++++++++++++++++------------- inspector/render.js | 6 +- 4 files changed, 129 insertions(+), 49 deletions(-) diff --git a/inspector/codec.js b/inspector/codec.js index 34541541..9f994ed0 100644 --- a/inspector/codec.js +++ b/inspector/codec.js @@ -362,7 +362,7 @@ const encodeWalkto = ({ fromSide, offset }) => { return encodeSide(fromSide) | (offset & 0xfc) } -const decodeAnimations = (data, startEndTableOff, firstCelOff, stateCount) => { +const decodeAnimations = (data, startEndTableOff, nextBlockOff, stateCount) => { const animations = [] // 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. @@ -373,7 +373,7 @@ const decodeAnimations = (data, startEndTableOff, firstCelOff, stateCount) => { // heuristic rather than failing to parse any animation data. // It's possible for there to be no frames, which is represented by an offset of 0 (no_animation) if (startEndTableOff != 0) { - for (let frameOff = startEndTableOff; (startEndTableOff > firstCelOff) || (frameOff < firstCelOff); frameOff += 2) { + for (let frameOff = startEndTableOff; (startEndTableOff > nextBlockOff) || (frameOff < nextBlockOff); frameOff += 2) { // each animation is two bytes: the starting state, and the ending state // the first byte can have its high bit set to indicate that the animation should cycle const cycle = (data.getUint8(frameOff) & 0x80) != 0 @@ -388,12 +388,28 @@ const decodeAnimations = (data, startEndTableOff, firstCelOff, stateCount) => { return animations } +const decodeContentsXY = (data, off, nextBlockOff) => { + const offsets = [] + // The prop structure doesn't encode the capacity of an open container - the only way to know + // how big this block is without digging into other files is to use the offset of the first cel as + // a boundary, as, in practice, this should be true of all existing props. (An object's capacity is + // defined in beta.mud, but that's per-object, not per-image. Building that association would be more + // complex than is needed here.) + // Non-containers and closed containers will have 0 here (no_cont). + if (off != 0) { + for (let xyOff = off; xyOff < nextBlockOff; xyOff += 2) { + offsets.push({ x: data.getInt8(xyOff), y: data.getInt8(xyOff + 1) }) + } + } + return offsets +} + export const decodeProp = (data) => { const prop = { data: data, howHeld: decodeHowHeld(data.getUint8(0)), colorBitmask: data.getUint8(1), - containerXYOff: data.getUint8(3), // TODO: parse this when nonzero + contentsInFront: (data.getUint8(3) & 0x80) == 0, walkto: { left: decodeWalkto(data.getUint8(4)), right: decodeWalkto(data.getUint8(5)), yoff: data.getInt8(6) }, celmasks: [], cels: [] @@ -426,7 +442,9 @@ export const decodeProp = (data) => { prop.cels.push(decodeCel(new DataView(data.buffer, celOff), (prop.colorBitmask & celbit) != 0)) allCelsMask = (allCelsMask << 1) & 0xff } - prop.animations = decodeAnimations(data, graphicStateOff, firstCelOff, stateCount) + const contentsXYOff = data.getUint8(3) & 0x7f + prop.animations = decodeAnimations(data, graphicStateOff, contentsXYOff == 0 ? firstCelOff : contentsXYOff, stateCount) + prop.contentsXY = decodeContentsXY(data, contentsXYOff, firstCelOff) return prop } diff --git a/inspector/navigate.html b/inspector/navigate.html index 6f20f251..fb54339a 100644 --- a/inspector/navigate.html +++ b/inspector/navigate.html @@ -20,7 +20,7 @@ import { html } from "./view.js" import { h, render } from "preact" import { useState, useId, useCallback, useMemo } from "preact/hooks" - import { regionView, regionSearch, itemView } from "./region.js" + import { regionView, regionSearch, itemView, standaloneItemView } from "./region.js" import { contextMap, useJson, useHabitatJson } from "./data.js" import { direction } from "./view.js" import { Scale } from "./render.js" @@ -40,7 +40,14 @@ if (filename) { const obj = useHabitatJson(filename).find(o => o.ref == dir) if (obj) { - return html`through
<${Scale.provider} value="1"><${itemView} object=${obj} standalone="true"/>
` + return html` + through +
+ <${Scale.provider} value="1"> + <${itemView} object=${obj} viewer=${standaloneItemView}/> + +
+
` } } } diff --git a/inspector/region.js b/inspector/region.js index 80a23129..2fc4b637 100644 --- a/inspector/region.js +++ b/inspector/region.js @@ -138,11 +138,12 @@ export const propFromMod = (mod, ref) => { } return fnAugment ? useTrap(ref, image.filename, fnAugment) : useBinary(image.filename, decodeProp, null) } -const propLocationFromMod = (prop, mod) => { - const x = (mod.x > 208 ? signedByte(mod.x) : mod.x) / 4 - const y = mod.y % 128 - const zIndex = mod.y > 127 ? (128 + (256 - mod.y)) : mod.y - return [prop.isTrap ? 0 : x, y, zIndex] + +const propLocationFromObjectXY = (prop, modX, modY) => { + const x = (modX > 208 ? signedByte(modX) : modX) / 4 + const y = modY % 128 + const zIndex = (modY > 127 ? (128 + (256 - modY)) : modY) * 2 + return [prop.isTrap ? 0 : x, y, prop.contentsInFront ? zIndex : zIndex + 1] } const colorsFromMod = (mod) => { @@ -158,60 +159,109 @@ const colorsFromMod = (mod) => { return colors } -const unwrappedItemView = ({ object, standalone }) => { - const scale = useContext(Scale) - const mod = object.mods[0] - const prop = propFromMod(mod, object.ref) - if (!prop) { - return null - } - const flipHorizontal = ((mod.orientation ?? 0) & 0x01) != 0 - const grState = mod.gr_state ?? 0 - const regionSpace = { minX: 0, minY: 0, maxX: 160 / 4, maxY: 127 } +const propFramesFromMod = (prop, mod) => { const colors = colorsFromMod(mod) const frames = useMemo(() => { + const flipHorizontal = ((mod.orientation ?? 0) & 0x01) != 0 + const grState = mod.gr_state ?? 0 if (prop.animations.length > 0) { return framesFromPropAnimation(prop.animations[grState], prop, { colors, flipHorizontal }) } else { return [frameFromCels(celsFromMask(prop, prop.celmasks[grState]), { colors, flipHorizontal })] } }, [prop, mod, colors.charset]) - - const [propX, propY, propZ] = propLocationFromMod(prop, mod) - const objectSpace = translateSpace(compositeSpaces(frames), propX, propY) - const [x, y] = topLeftCanvasOffset(regionSpace, objectSpace) - const style = !standalone ? `position: absolute; left: ${x * scale}px; top: ${y * scale}px; z-index: ${propZ}` : "" - const image = html`<${animatedDiv} frames=${frames}/>` - const connection = mod.connection && contextMap()[mod.connection] - return html` -
- ${connection ? html`${image}` : image} -
` + return frames } export const itemView = (props) => { return html` <${catcher} filename=${props.object.ref}> - <${unwrappedItemView} ...${props}/> + <${props.viewer} ...${props}/> + ` +} + +export const standaloneItemView = ({ object }) => { + const mod = object.mods[0] + const prop = propFromMod(mod, object.ref) + if (!prop) { + return null + } + return html`<${animatedDiv} frames=${propFramesFromMod(prop, mod)}/>` +} + +export const positionedInRegion = ({ space, z, children }) => { + const scale = useContext(Scale) + const regionSpace = { minX: 0, minY: 0, maxX: 160 / 4, maxY: 127 } + const [x, y] = topLeftCanvasOffset(regionSpace, space) + const style =`position: absolute; left: ${x * scale}px; top: ${y * scale}px; z-index: ${z}` + return html`
${children}
` +} + +export const containedItemView = ({ object, containerProp, containerMod, containerSpace }) => { + const mod = object.mods[0] + const prop = propFromMod(mod, object.ref) + if (!prop || containerProp.contentsXY.length < mod.y) { + return null + } + const [containerX, containerY, containerZ] = propLocationFromObjectXY(containerProp, containerMod.x, containerMod.y) + const { x: offsetX, y: offsetY } = containerProp.contentsXY[mod.y] + const flipHorizontal = ((mod.orientation ?? 0) & 0x01) != 0 + // offsets are relative to `cel_x_origin` / `cel_y_origin`, which is in "habitat space" but with + // the y axis inverted (see render.m:115-121) + const frames = propFramesFromMod(prop, mod) + const frameSpace = compositeSpaces(frames) + // if the contents are drawn in front, the container has its origin offset by the offset of its first cel. + const originX = containerProp.contentsInFront ? containerSpace.xOrigin + 2 : 0 + const originY = containerProp.contentsInFront ? containerSpace.yOrigin : 0 + const x = containerX + (flipHorizontal ? -originX + offsetX : originX - offsetX) + const y = containerY - (offsetY + originY) + const z = containerProp.contentsInFront ? containerZ + 1 : containerZ - 1 + const objectSpace = translateSpace(frameSpace, x, y) + return html` + <${positionedInRegion} space=${objectSpace} z=${z}> + <${animatedDiv} frames=${frames}/> ` } +export const itemInteraction = ({ mod, children }) => { + const connection = mod.connection && contextMap()[mod.connection] + if (connection) { + return html`${children}` + } + return children +} + +export const regionItemView = ({ object, contents = [] }) => { + const mod = object.mods[0] + const prop = propFromMod(mod, object.ref) + if (!prop) { + return null + } + const [propX, propY, propZ] = propLocationFromObjectXY(prop, mod.x, mod.y) + const frames = propFramesFromMod(prop, mod) + const objectSpace = translateSpace(compositeSpaces(frames), propX, propY) + const result = [html` + <${positionedInRegion} key=${object.ref} space=${objectSpace} z=${propZ}> + <${itemInteraction} mod=${mod}> + <${animatedDiv} frames=${frames}/> + + `] + if (prop.contentsXY.length > 0) { + for (const item of contents) { + result.push(html`<${containedItemView} key=${item.ref} object=${item} containerProp=${prop} containerMod=${mod} containerSpace=${frames[0]}/>`) + } + } + return result +} + export const regionView = ({ filename }) => { const scale = useContext(Scale) const objects = useHabitatJson(filename, []) - let regionRef = null - const items = objects.flatMap(obj => { - if (obj.type == "context") { - regionRef = obj.ref - } else if (obj.type != "item") { - logError(`Unknown object type ${obj.type}`, obj.ref) - } else if (regionRef && obj.in != regionRef) { - logError(`Object is inside container ${obj.in}; not yet supported`, obj.ref) - } else { - return [html`<${itemView} object=${obj}/>`] - } - return [] - }) + const regionRef = objects.find(o => o.type === "context")?.ref + const items = objects + .filter(obj => obj.type === "item" && obj.in === regionRef) + .map(obj => html`<${itemView} viewer=${regionItemView} object=${obj} contents=${objects.filter(o => o.in === obj.ref)} key=${obj.ref}/>`) + return html`
${items} @@ -279,8 +329,9 @@ export const objectDetails = ({ filename }) => { } } details = html` - ${image.filename} - <${itemView} object=${obj} standalone="true"/>` + + ${image.filename}
<${itemView} object=${obj} viewer=${standaloneItemView}/> +
` } } return html` diff --git a/inspector/render.js b/inspector/render.js index 323f2634..df0a1616 100644 --- a/inspector/render.js +++ b/inspector/render.js @@ -260,7 +260,9 @@ export const frameFromText = (x, y, bytes, charset, pattern, fineXOffset, colors return compositeLayers(layers) } -// Habitat's coordinate space consistently has y=0 for the bottom, and increasing y means going up +// We try to consistently model Habitat's coordinate space in our rendering code as y=0 for the bottom, with increasing y meaning going up. +// However, the graphics code converts this internally to a coordinate space where increasing y means going down, and many internal +// coordinates (cel offsets, etc.) assume this. export const frameFromCels = (cels, { colors: celColors, paintOrder, firstCelOrigin = true, flipHorizontal }) => { if (cels.length == 0) { return null @@ -319,6 +321,8 @@ export const frameFromCels = (cels, { colors: celColors, paintOrder, firstCelOri frame.minX = -maxX + 1 frame.maxX = -minX + 1 } + frame.xOrigin = xCorrect + frame.yOrigin = yCorrect return translateSpace(frame, -xCorrect, -yCorrect) }