diff --git a/README.md b/README.md index 1ad55f4..1b245ac 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ It currently support: - Rooms (partially) - Room graphics - Prepositions +- Title screens The following version are supported: diff --git a/src/components/Header.js b/src/components/Header.js index d5c05e2..6b25eac 100644 --- a/src/components/Header.js +++ b/src/components/Header.js @@ -14,7 +14,7 @@ import { import meteor from '../assets/meteor.png'; const navigation = [ - { name: 'Rooms', href: '/rooms/1' }, + { name: 'Screens', href: '/rooms/1' }, { name: 'Gfx', href: '/roomgfx/0' }, { name: 'Scripts', href: '/scripts/1' }, { name: 'Prepositions', href: '/preps' }, diff --git a/src/components/Room.js b/src/components/Room.js index 2bc9b79..5c9b147 100644 --- a/src/components/Room.js +++ b/src/components/Room.js @@ -1,6 +1,6 @@ import MainHeader from './MainHeader'; import ResourceMetadata from './ResourceMetadata'; -import RoomCanvasContainer from '../containers/RoomCanvasContainer'; +import ScreenCanvasContainer from '../containers/ScreenCanvasContainer'; import HoveredObjects from './HoveredObjects'; const Room = ({ @@ -24,10 +24,10 @@ const Room = ({
- { +const RoomGfx = ({ + baseTiles, + nametable, + objectImages, + roomgfc, + type = 'room', +}) => { return (
+ {baseTiles.gfx.length ? ( + + Base tileset ({baseTiles.gfx.length / 8 / 2} tiles) + + + ) : null} - Base tileset ({baseTiles.gfx.length / 8 / 2} tiles) - - - + to={ + type === 'room' + ? `/roomgfx/${nametable.tileset}` + : `/titlegfx/${nametable.tileset}` + }> Tileset {nametable.tileset} ({roomgfc.gfx.length / 8 / 2} tiles) { +const RoomTabs = ({ + currentTab, + setCurrentTab, + allowList = ['Palettes', 'Tilesets', 'Scripts'], +}) => { const tabs = [ { name: 'Palettes', current: currentTab === 'Palettes' }, { name: 'Tilesets', current: currentTab === 'Tilesets' }, { name: 'Scripts', current: currentTab === 'Scripts' }, - ]; + ].filter(({ name }) => allowList.includes(name)); return (
diff --git a/src/components/RoomsList.js b/src/components/RoomsList.js index 074d289..40446a6 100644 --- a/src/components/RoomsList.js +++ b/src/components/RoomsList.js @@ -1,11 +1,11 @@ import ColumnListHeader from './ColumnListHeader'; import ColumnListItem from './ColumnListItem'; -const RoomsList = ({ rooms, currentId }) => { +const RoomsList = ({ items, currentId }) => { return ( <> Rooms - {rooms.map(({ metadata, header }) => { + {items.map(({ metadata, header }) => { if (!header) { // Some rooms are empty. return null; diff --git a/src/components/TitlesList.js b/src/components/TitlesList.js new file mode 100644 index 0000000..14b56db --- /dev/null +++ b/src/components/TitlesList.js @@ -0,0 +1,25 @@ +import ColumnListHeader from './ColumnListHeader'; +import ColumnListItem from './ColumnListItem'; + +const TitlesList = ({ items, currentId }) => { + return ( + <> + Titles + {items.map(({ metadata }) => { + const selected = metadata.id === currentId; + const path = `/titles/${metadata.id}`; + const label = `Title ${metadata.id}`; + + return ( + + {label} + + ); + })} + + ); +}; + +export default TitlesList; diff --git a/src/containers/ResourceExplorer.js b/src/containers/ResourceExplorer.js index da24dc4..cfcdc4e 100644 --- a/src/containers/ResourceExplorer.js +++ b/src/containers/ResourceExplorer.js @@ -3,6 +3,7 @@ import RoomsContainer from './RoomsContainer'; import GfxContainer from './GfxContainer'; import PrepositionsContainer from './PrepositionsContainer'; import RomMapContainer from './RomMapContainer'; +import TitlesContainer from './TitlesContainer'; import SettingsContainer from './SettingsContainer'; import ScriptContainer from './ScriptContainer'; @@ -18,21 +19,41 @@ const ResourceExplorer = ({ rom, res, resources }) => { element={ }> } /> + + }> + + } + /> + { - const { roomId } = useParams(); +const RoomsContainer = ({ rooms, titles, roomgfx, globdata }) => { + const isRoom = !!useMatch('/rooms/:id'); + const { id } = useParams(); const [hoveredObject, setHoveredObject] = useState(null); const [selectedObjects, setSelectedObjects] = useState([]); const [hoveredBox, setHoveredBox] = useState(null); const [currentTab, setCurrentTab] = useState('Palettes'); const [room, setRoom] = useState(null); - const currentRoomId = - typeof roomId === 'undefined' ? null : parseInt(roomId, 10); + const currentId = typeof id === 'undefined' ? null : parseInt(id, 10); const baseTiles = roomgfx?.find(({ metadata }) => metadata.id === 0); let roomgfc = roomgfx?.find( ({ metadata }) => metadata.id === room?.nametable?.tileset, @@ -29,7 +30,7 @@ const RoomsContainer = ({ rooms, roomgfx, globdata }) => { useEffect(() => { const room = - rooms.find(({ metadata }) => metadata.id === currentRoomId) || null; + rooms.find(({ metadata }) => metadata.id === currentId) || null; setRoom(room); // Clear the selected objects when the room changes. @@ -40,7 +41,7 @@ const RoomsContainer = ({ rooms, roomgfx, globdata }) => { selectedObjects[i] = !!(initialState & 0b10000000); } setSelectedObjects(selectedObjects); - }, [roomId]); + }, [id]); const setSelectedObjectState = (id, state) => { const newSelectedObjects = [...selectedObjects]; @@ -75,8 +76,12 @@ const RoomsContainer = ({ rooms, roomgfx, globdata }) => { <> + {(room?.objectImages?.length || room?.boxes?.length) && ( diff --git a/src/containers/RoomCanvasContainer.js b/src/containers/ScreenCanvasContainer.js similarity index 69% rename from src/containers/RoomCanvasContainer.js rename to src/containers/ScreenCanvasContainer.js index 9803cdc..4402f7a 100644 --- a/src/containers/RoomCanvasContainer.js +++ b/src/containers/ScreenCanvasContainer.js @@ -2,28 +2,31 @@ import { useRef, useState, useEffect } from 'react'; import { clsx } from 'clsx'; import { getPalette } from '../lib/paletteUtils'; -const RoomCanvasContainer = ({ - room, +// Display a screen on a canvas. Used by rooms and title screens. + +const ScreenCanvasContainer = ({ + screen, baseTiles, - roomgfc, + gfc, selectedObjects, hoveredBox, + crop = true, zoom = 1, }) => { const canvasRef = useRef(null); const [isComputing, setIsComputing] = useState(true); - const { width, height } = room.header; + const { width, height } = screen.header; useEffect(() => { const canvas = canvasRef.current; const ctx = canvas.getContext('2d'); setTimeout(() => { - draw(ctx, room, baseTiles, roomgfc, selectedObjects); - drawBoxes(ctx, room.boxes, hoveredBox); + draw(ctx, screen, baseTiles, gfc, selectedObjects, crop); + drawBoxes(ctx, screen.boxes, hoveredBox); setIsComputing(false); }); - }, [room, selectedObjects, hoveredBox]); + }, [screen, selectedObjects, hoveredBox, crop]); return ( { +const draw = ( + ctx, + room, + baseTiles = { gfx: [] }, + roomgfc, + selectedObjects = [], + crop, +) => { + const { width, height } = room.header; const { nametableObj, palette } = room.nametable; const attributes = room.attributes; - const baseTilesNum = baseTiles.gfx.length / 8 / 2; + const baseTilesNum = baseTiles?.gfx?.length / 8 / 2; const nametableObjCopy = nametableObj.map((arr) => arr.slice()); // Overwrite tiles with selected object. @@ -70,20 +81,31 @@ const draw = (ctx, room, baseTiles, roomgfc, selectedObjects) => { } // Now generate the image of the room. - for (let sprY = 0; sprY < 16; sprY++) { - for (let sprX = 2; sprX < 62; sprX++) { + for (let sprY = 0; sprY < height; sprY++) { + for (let sprX = 0; sprX < width; sprX++) { let tile = nametableObjCopy[sprY][sprX]; - let gfx = baseTiles.gfx; + let gfx = baseTiles?.gfx; if (tile >= baseTilesNum) { tile -= baseTilesNum; gfx = roomgfc.gfx; } - const paletteId = - (attributes[((sprY << 2) & 0x30) | ((sprX >> 2) & 0xf)] >> - (((sprY & 2) << 1) | (sprX & 2))) & - 0x3; + let paletteId; + + if (width === 32) { + // Title screen. + paletteId = + (attributes[((sprY & 0xfffc) << 1) + (sprX >> 2)] >> + (((sprY & 2) << 1) | (sprX & 2))) & + 0x3; + } else { + // SCUMM room. + paletteId = + (attributes[((sprY << 2) & 0x30) | ((sprX >> 2) & 0xf)] >> + (((sprY & 2) << 1) | (sprX & 2))) & + 0x3; + } const pal = getPalette([ palette[paletteId * 4], palette[paletteId * 4 + 1], @@ -99,15 +121,19 @@ const draw = (ctx, room, baseTiles, roomgfc, selectedObjects) => { const val = (n1 & mask ? 1 : 0) | ((n2 & mask ? 1 : 0) << 1); ctx.fillStyle = pal[val]; - ctx.fillRect((sprX - 2) * 8 + 7 - k, sprY * 8 + j, 1, 1); + if (crop) { + ctx.fillRect((sprX - 2) * 8 + 7 - k, sprY * 8 + j, 1, 1); + } else { + ctx.fillRect(sprX * 8 + 7 - k, sprY * 8 + j, 1, 1); + } } } } } }; -const drawBoxes = (ctx, boxes, hoveredBox) => { - if (hoveredBox === null) { +const drawBoxes = (ctx, boxes = null, hoveredBox) => { + if (boxes === null || hoveredBox === null) { return; } @@ -135,4 +161,4 @@ const drawBox = (ctx, { uy, ly, ulx, urx, llx, lrx }) => { ctx.lineTo((ulx - 1) * 8, (uy - 1) * 2); }; -export default RoomCanvasContainer; +export default ScreenCanvasContainer; diff --git a/src/containers/TitlesContainer.js b/src/containers/TitlesContainer.js new file mode 100644 index 0000000..2998e18 --- /dev/null +++ b/src/containers/TitlesContainer.js @@ -0,0 +1,110 @@ +import { useState, useEffect } from 'react'; +import { useMatch, useParams } from 'react-router-dom'; +import PrimaryColumn from '../components/PrimaryColumn'; +import Main from '../components/Main'; +import RoomsList from '../components/RoomsList'; +import TitlesList from '../components/TitlesList'; +import ScreenCanvasContainer from './ScreenCanvasContainer'; +import RoomTabs from '../components/RoomTabs'; +import Palettes from '../components/Palettes'; +import RoomGfx from '../components/RoomGfx'; +import MainHeader from '../components/MainHeader'; +import ResourceMetadata from '../components/ResourceMetadata'; + +const TitlesContainer = ({ rooms, titles }) => { + const isRoom = !!useMatch('/rooms/:id'); + const { id } = useParams(); + const [currentTab, setCurrentTab] = useState('Palettes'); + const [title, setTitle] = useState(null); + const zoom = 2; + + const currentId = typeof id === 'undefined' ? null : parseInt(id, 10); + + useEffect(() => { + const title = titles[id]; + + // Hack: so it has the same shape than rooms. + title.header = { + width: title.width, + height: title.height, + }; + title.nametable = { + tileset: id, + palette: title.palette, + nametableObj: title.nametableObj, + }; + + setTitle(title); + }, [id]); + + const updatePalette = (i, colourId) => { + const newRoom = structuredClone(title); + if (i % 4 === 0) { + // Keep the first colours in sync. + for (let i = 0; i < 16; i += 4) { + newRoom.nametable.palette[i] = colourId; + } + } else { + newRoom.nametable.palette[i] = colourId; + } + setTitle(newRoom); + }; + + if (!title) { + return null; + } + + return ( + <> + + + + +
+ {!title ? ( +

Titles

+ ) : ( + <> + + + + + + {currentTab === 'Palettes' && ( + + )} + {currentTab === 'Tilesets' && ( + + )} + + )} +
+ + ); +}; + +export default TitlesContainer; diff --git a/src/lib/parser/parseTitles.js b/src/lib/parser/parseTitles.js index dc05939..a3d4551 100644 --- a/src/lib/parser/parseTitles.js +++ b/src/lib/parser/parseTitles.js @@ -21,8 +21,6 @@ const parseTitles = (arrayBuffer, i = 0, offset = 0) => { // Parse tileset. const numberOfTiles = parser.getUint8() + 1; - console.log('numberOfTiles', numberOfTiles); - const gfx = []; let n = 0; while (n < numberOfTiles * 16) { @@ -46,12 +44,98 @@ const parseTitles = (arrayBuffer, i = 0, offset = 0) => { // metadata.decompressedSize = gfx.length; + // Parse gfx nametable. + const unk3 = parser.getUint16(); + const unk4 = parser.getUint8(); + const width = parser.getUint8() + 1; + const height = parser.getUint8() + 1; + + assert(unk3 === 1, 'Unknown 3 is not 1.'); + assert(width === 32, 'Title width is not 32.'); + assert(height === 30, 'Title height is not 30.'); + + const nametable = Array(width * height); + n = 0; + while (n < nametable.length) { + const loop = parser.getUint8(); + if (loop & 0x80) { + for (let j = 0; j < (loop & 0x7f); j++) { + nametable[n++] = parser.getUint8(); + } + } else { + const data = parser.getUint8(); + for (let j = 0; j < (loop & 0x7f); j++) { + nametable[n++] = data; + } + } + } + + // Slice the nametableObj so it is formatted like rooms'. + const nametableObj = new Array(height); + for (let i = 0; i < height; i++) { + nametableObj[i] = nametable.slice(i * width, (i + 1) * width); + } + + // Parse gfx attrtable. + const unk5 = parser.getUint16(); + const unk6 = parser.getUint8(); + const attrWidth = parser.getUint8() + 1; + const attrHeight = parser.getUint8() + 1; + + assert(unk5 === 0xc002, 'Unknown 5 is not 1.'); + assert(unk6 === 0x23, 'Unknown 6 is not 0x23.'); + assert(attrWidth === 8, 'attrWidth is not 8.'); + assert(attrHeight === 8, 'attrHeight is not 8.'); + + const attributes = Array(attrWidth * attrHeight).fill(0); + n = 0; + while (n < attributes.length) { + const loop = parser.getUint8(); + if (loop & 0x80) { + for (let j = 0; j < (loop & 0x7f); j++) { + attributes[n++] = parser.getUint8(); + } + } else { + const data = parser.getUint8(); + for (let j = 0; j < (loop & 0x7f); j++) { + attributes[n++] = data; + } + } + } + + // Parse palette. + const stepNum = parser.getUint8(); + + assert(stepNum === 3, 'stepNum is not 3.'); + + const palette = []; + for (let i = 0; i < 16; i++) { + palette[i] = parser.getUint8(); + } + + const endOfData = parser.getUint8(); + + assert(endOfData === 0xff, 'endOfData is not 0xff.'); + return { metadata, - unk1, - unk2, numberOfTiles, gfx, + width, + height, + nametableObj, + attrWidth, + attrHeight, + attributes, + stepNum, + palette, + endOfData, + unk1, + unk2, + unk3, + unk4, + unk5, + unk6, }; }; diff --git a/src/lib/parser/room/parseRoomNametable.js b/src/lib/parser/room/parseRoomNametable.js index fb54a1a..44c7d6d 100644 --- a/src/lib/parser/room/parseRoomNametable.js +++ b/src/lib/parser/room/parseRoomNametable.js @@ -12,7 +12,7 @@ const parseRoomNametable = (arrayBuffer, offset = 0, width = 0) => { } const nametableObj = Array(16); - for (let i = 0; i < 16; i++) { + for (let i = 0; i < nametableObj.length; i++) { nametableObj[i] = Array(64).fill(0); nametableObj[i][0] = 0; nametableObj[i][1] = 0;