From d8db4e6353f895c25470c5fd573bd9c7adfb65c4 Mon Sep 17 00:00:00 2001 From: "Guillaume C. Marty" Date: Sat, 15 Jun 2024 10:41:33 -0500 Subject: [PATCH] =?UTF-8?q?Display=20parsed=20costumes=20=F0=9F=91=94.=20(?= =?UTF-8?q?#22)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [WIP] Add a page to view the costumes. * Partial fix for costume parsing (#21) * Partial fix for costume parsing * A few refinements * Minor refactoring. * Set the number of frames. * Fix sprites rendering. * Improve display. * Bit of cleaning. * Add support for different costume sets. --------- Co-authored-by: Gamaiel Zavala --- src/components/CostumesList.js | 29 +++++ src/components/Header.js | 1 + src/containers/CostumeCanvasContainer.js | 147 +++++++++++++++++++++++ src/containers/CostumesContainer.js | 93 ++++++++++++++ src/containers/GfxContainer.js | 1 + src/containers/PrepositionsContainer.js | 1 + src/containers/ResourceExplorer.js | 45 +++++-- src/containers/ScriptContainer.js | 1 + src/containers/TitlesContainer.js | 1 + src/lib/parser/costumes/parseSprdesc.js | 4 +- 10 files changed, 313 insertions(+), 10 deletions(-) create mode 100644 src/components/CostumesList.js create mode 100644 src/containers/CostumeCanvasContainer.js create mode 100644 src/containers/CostumesContainer.js diff --git a/src/components/CostumesList.js b/src/components/CostumesList.js new file mode 100644 index 0000000..11b852b --- /dev/null +++ b/src/components/CostumesList.js @@ -0,0 +1,29 @@ +import ColumnListHeader from './ColumnListHeader'; +import ColumnListItem from './ColumnListItem'; + +const CostumesList = ({ costumeSets, currentSetId, currentId }) => { + return ( + <> + {costumeSets.map((costumeSet, costumeSetId) => ( +
+ Costume set {costumeSetId} + {costumeSet.sprdesc.map((unused, id) => { + const selected = costumeSetId === currentSetId && id === currentId; + const path = `/costumes/${costumeSetId}/${id}`; + const label = `Costume ${id}`; + + return ( + + {label} + + ); + })} +
+ ))} + + ); +}; + +export default CostumesList; diff --git a/src/components/Header.js b/src/components/Header.js index cad8dd5..76fd111 100644 --- a/src/components/Header.js +++ b/src/components/Header.js @@ -17,6 +17,7 @@ import meteor from '../assets/meteor.png'; const navigation = [ { name: 'Screens', href: '/rooms/1' }, + { name: 'Costumes', href: '/costumes/0/0' }, { name: 'Gfx', href: '/roomgfx/0' }, { name: 'Scripts', href: '/scripts/1' }, { name: 'Prepositions', href: '/preps' }, diff --git a/src/containers/CostumeCanvasContainer.js b/src/containers/CostumeCanvasContainer.js new file mode 100644 index 0000000..4479437 --- /dev/null +++ b/src/containers/CostumeCanvasContainer.js @@ -0,0 +1,147 @@ +import { useRef, useState, useEffect } from 'react'; +import { clsx } from 'clsx'; +import { getPalette } from '../lib/paletteUtils'; + +// Display a costume on a canvas. + +// prettier-ignore +const darkpalette = [ + 0x2d, 0x1d, 0x2d, 0x3d, + 0x2d, 0x1d, 0x2d, 0x3d, + 0x2d, 0x1d, 0x2d, 0x3d, + 0x2d, 0x1d, 0x2d, 0x3d, +]; + +const CostumeCanvasContainer = ({ + id, + frame, + gfx, + sprdesc, + sproffs, + sprlens, + sprdata, + sprpals, + zoom = 1, +}) => { + const canvasRef = useRef(null); + const [isComputing, setIsComputing] = useState(true); + + const desc = sprdesc[id]; + // this was 3 bytes per sprite in the data but has been parsed down to 1 byte + const offset = sproffs[desc + frame] / 3; + const spritesNum = sprlens[desc + frame]; + const palette = sprpals.palette; + + // Compute the bounding box. + let left = 239; + let right = 0; + let top = 239; + let bottom = 0; + for (let i = 0; i < spritesNum; i++) { + const { x, y } = sprdata[offset + i]; + + left = Math.min(left, x); + right = Math.max(right, x + 8); + top = Math.min(top, y); + bottom = Math.max(bottom, y + 8); + } + + const width = right - left; + const height = bottom - top; + + useEffect(() => { + const canvas = canvasRef.current; + const ctx = canvas.getContext('2d'); + + setTimeout(() => { + draw( + ctx, + gfx.gfx, + sprdata, + offset, + spritesNum, + palette, + left, + top, + width, + height, + ); + setIsComputing(false); + }); + }, [ + frame, + gfx, + sprdata, + offset, + spritesNum, + palette, + left, + top, + width, + height, + ]); + + return ( + + ); +}; + +const draw = ( + ctx, + gfx, + sprdata, + offset, + spritesNum, + palette, + left, + top, + width, + height, +) => { + // Clear the canvas. + ctx.fillStyle = 'lightgrey'; + ctx.fillRect(0, 0, width, height); + + for (let i = 0; i < spritesNum; i++) { + const { x, y, tile, flip, paletteId } = sprdata[offset + i]; + + const pal = getPalette([ + palette[paletteId], + palette[paletteId + 1], + palette[paletteId + 2], + palette[paletteId + 3], + ]); + + for (let j = 0; j < 8; j++) { + const n1 = gfx[tile * 2 * 8 + j]; + const n2 = gfx[(tile * 2 + 1) * 8 + j]; + for (let k = 0; k < 8; k++) { + const mask = 1 << k; + const val = (n1 & mask ? 1 : 0) | ((n2 & mask ? 1 : 0) << 1); + + // Skip the transparent palette colour. + if (val === 0) { + continue; + } + + ctx.fillStyle = pal[val]; + if (flip) { + ctx.fillRect(k + x - left, j + y - top, 1, 1); + } else { + ctx.fillRect(7 - k + x - left, j + y - top, 1, 1); + } + } + } + } +}; + +export default CostumeCanvasContainer; diff --git a/src/containers/CostumesContainer.js b/src/containers/CostumesContainer.js new file mode 100644 index 0000000..e3eb355 --- /dev/null +++ b/src/containers/CostumesContainer.js @@ -0,0 +1,93 @@ +import { useParams } from 'react-router-dom'; +import PrimaryColumn from '../components/PrimaryColumn'; +import CostumesList from '../components/CostumesList'; +import Main from '../components/Main'; +import MainHeader from '../components/MainHeader'; +import ResourceMetadata from '../components/ResourceMetadata'; +import CostumeCanvasContainer from './CostumeCanvasContainer'; + +// @todo Parse it from 3DAED-3DB05 instead of hardcoding. +// prettier-ignore +const costumeIdLookupTable = [ + 0x00, 0x03, 0x01, 0x06, 0x08, + 0x02, 0x00, 0x07, 0x0c, 0x04, + 0x09, 0x0a, 0x12, 0x0b, 0x14, + 0x0d, 0x11, 0x0f, 0x0e, 0x10, + 0x17, 0x00, 0x01, 0x05, 0x16, +]; + +const CostumesContainer = ({ + costumegfx, + costumes, + sprpals, + sprdesc, + sproffs, + sprlens, + sprdata, +}) => { + const { setId, id } = useParams(); + + const currentSetId = + typeof setId === 'undefined' ? null : parseInt(setId, 10); + const currentId = typeof id === 'undefined' ? null : parseInt(id, 10); + const costume = + costumes.find(({ metadata }) => metadata.id === currentId) || null; + const costumeId = + currentSetId === 0 ? costumeIdLookupTable[currentId] : currentId; + + const getFramesNumbersFromCostumeId = (costumeId = 0) => { + if (costumeId === sprdesc[currentSetId].sprdesc.length - 1) { + // @todo Find a better way than hardcoding it. + return currentSetId === 0 ? 2 : 1; + } + + return ( + sprdesc[currentSetId].sprdesc[costumeId + 1] - + sprdesc[currentSetId].sprdesc[costumeId] + ); + }; + + const frameNum = getFramesNumbersFromCostumeId(costumeId); + + if (!costume) { + return null; + } + + return ( + <> + + + + +
+ + + +
+ {Array(frameNum) + .fill() + .map((unused, frame) => ( + + ))} +
+
+ + ); +}; + +export default CostumesContainer; diff --git a/src/containers/GfxContainer.js b/src/containers/GfxContainer.js index 4661410..957dd98 100644 --- a/src/containers/GfxContainer.js +++ b/src/containers/GfxContainer.js @@ -41,6 +41,7 @@ const GfxContainer = ({ titlegfx, costumegfx, roomgfx }) => { currentId={isRoomGfx ? currentId : null} /> +
{ lang={lang} /> +
diff --git a/src/containers/ResourceExplorer.js b/src/containers/ResourceExplorer.js index 00cd0cf..2c3a17e 100644 --- a/src/containers/ResourceExplorer.js +++ b/src/containers/ResourceExplorer.js @@ -1,6 +1,7 @@ import { Routes, Route } from 'react-router-dom'; import { useRom } from '../contexts/RomContext'; import RoomsContainer from './RoomsContainer'; +import CostumesContainer from './CostumesContainer'; import GfxContainer from './GfxContainer'; import PrepositionsContainer from './PrepositionsContainer'; import RomMapContainer from './RomMapContainer'; @@ -17,6 +18,24 @@ const ResourceExplorer = () => { return ( + + }> + + } + /> + { /> }> } /> diff --git a/src/containers/ScriptContainer.js b/src/containers/ScriptContainer.js index 1080e00..d7ac84f 100644 --- a/src/containers/ScriptContainer.js +++ b/src/containers/ScriptContainer.js @@ -19,6 +19,7 @@ const ScriptContainer = ({ scripts }) => { currentId={currentId} /> +
{!script ? (

Scripts

diff --git a/src/containers/TitlesContainer.js b/src/containers/TitlesContainer.js index ca83732..e391634 100644 --- a/src/containers/TitlesContainer.js +++ b/src/containers/TitlesContainer.js @@ -64,6 +64,7 @@ const TitlesContainer = ({ rooms, titles }) => { titles={titles} /> +
{!title ? (

Titles

diff --git a/src/lib/parser/costumes/parseSprdesc.js b/src/lib/parser/costumes/parseSprdesc.js index 6e4af57..f7013b8 100644 --- a/src/lib/parser/costumes/parseSprdesc.js +++ b/src/lib/parser/costumes/parseSprdesc.js @@ -13,8 +13,8 @@ const parseSprdesc = (arrayBuffer, i = 0, offset = 0) => { sprdesc.push(parser.getUint16()); } - // For some reason, the last element is a Uint8, probably unused. - sprdesc.push(parser.getUint8()); + // For some reason, the last element is an unused Uint8. + parser.getUint8(); const map = { type: 'sprdesc',