Skip to content

Commit

Permalink
Parse nametable, attributes and palette from title screens 📺.
Browse files Browse the repository at this point in the history
  • Loading branch information
gmarty committed May 20, 2024
1 parent ecbf6f9 commit 5458860
Show file tree
Hide file tree
Showing 13 changed files with 346 additions and 58 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ It currently support:
- Rooms (partially)
- Room graphics
- Prepositions
- Title screens

The following version are supported:

Expand Down
2 changes: 1 addition & 1 deletion src/components/Header.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand Down
8 changes: 4 additions & 4 deletions src/components/Room.js
Original file line number Diff line number Diff line change
@@ -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 = ({
Expand All @@ -24,10 +24,10 @@ const Room = ({
<div
className="relative overflow-hidden"
style={{ maxWidth: width * 8 * zoom }}>
<RoomCanvasContainer
room={room}
<ScreenCanvasContainer
screen={room}
baseTiles={baseTiles}
roomgfc={roomgfc}
gfc={roomgfc}
selectedObjects={selectedObjects}
hoveredBox={hoveredBox}
zoom={zoom}
Expand Down
40 changes: 26 additions & 14 deletions src/components/RoomGfx.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,36 @@
import { Link } from 'react-router-dom';
import GfxCanvasContainer from '../containers/GfxCanvasContainer';

const RoomGfx = ({ baseTiles, nametable, objectImages, roomgfc }) => {
const RoomGfx = ({
baseTiles,
nametable,
objectImages,
roomgfc,
type = 'room',
}) => {
return (
<div className="flex gap-4 md:gap-5 xl:gap-6">
{baseTiles.gfx.length ? (
<Link
className="text-center text-sm"
to="/roomgfx/0">
Base tileset ({baseTiles.gfx.length / 8 / 2} tiles)
<GfxCanvasContainer
gfx={baseTiles.gfx}
nametable={nametable}
objectImages={objectImages}
nametableStart={0}
zoom={2}
/>
</Link>
) : null}
<Link
className="text-center text-sm"
to="/roomgfx/0">
Base tileset ({baseTiles.gfx.length / 8 / 2} tiles)
<GfxCanvasContainer
gfx={baseTiles.gfx}
nametable={nametable}
objectImages={objectImages}
nametableStart={0}
zoom={2}
/>
</Link>
<Link
className="text-center text-sm"
to={`/roomgfx/${nametable.tileset}`}>
to={
type === 'room'
? `/roomgfx/${nametable.tileset}`
: `/titlegfx/${nametable.tileset}`
}>
Tileset {nametable.tileset} ({roomgfc.gfx.length / 8 / 2} tiles)
<GfxCanvasContainer
gfx={roomgfc.gfx}
Expand Down
8 changes: 6 additions & 2 deletions src/components/RoomTabs.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { clsx } from 'clsx';

const RoomTabs = ({ currentTab, setCurrentTab }) => {
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 (
<div>
Expand Down
4 changes: 2 additions & 2 deletions src/components/RoomsList.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import ColumnListHeader from './ColumnListHeader';
import ColumnListItem from './ColumnListItem';

const RoomsList = ({ rooms, currentId }) => {
const RoomsList = ({ items, currentId }) => {
return (
<>
<ColumnListHeader>Rooms</ColumnListHeader>
{rooms.map(({ metadata, header }) => {
{items.map(({ metadata, header }) => {
if (!header) {
// Some rooms are empty.
return null;
Expand Down
25 changes: 25 additions & 0 deletions src/components/TitlesList.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import ColumnListHeader from './ColumnListHeader';
import ColumnListItem from './ColumnListItem';

const TitlesList = ({ items, currentId }) => {
return (
<>
<ColumnListHeader>Titles</ColumnListHeader>
{items.map(({ metadata }) => {
const selected = metadata.id === currentId;
const path = `/titles/${metadata.id}`;
const label = `Title ${metadata.id}`;

return (
<ColumnListItem
key={metadata.id}
path={selected ? null : path}>
{label}
</ColumnListItem>
);
})}
</>
);
};

export default TitlesList;
23 changes: 22 additions & 1 deletion src/containers/ResourceExplorer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -18,21 +19,41 @@ const ResourceExplorer = ({ rom, res, resources }) => {
element={
<RoomsContainer
rooms={resources.rooms}
titles={resources.titles}
roomgfx={resources.roomgfx}
globdata={resources.globdata[0]}
/>
}>
<Route
path=":roomId"
path=":id"
element={
<RoomsContainer
rooms={resources.rooms}
titles={resources.titles}
roomgfx={resources.roomgfx}
globdata={resources.globdata[0]}
/>
}
/>
</Route>
<Route
path="/titles"
element={
<TitlesContainer
rooms={resources.rooms}
titles={resources.titles}
/>
}>
<Route
path=":id"
element={
<TitlesContainer
rooms={resources.rooms}
titles={resources.titles}
/>
}
/>
</Route>
<Route
path="/roomgfx"
element={
Expand Down
23 changes: 14 additions & 9 deletions src/containers/RoomsContainer.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { useMatch, useParams } from 'react-router-dom';
import PrimaryColumn from '../components/PrimaryColumn';
import SecondaryColumn from '../components/SecondaryColumn';
import Main from '../components/Main';
import RoomsList from '../components/RoomsList';
import TitlesList from '../components/TitlesList';
import RoomsObjectList from '../components/RoomsObjectList';
import RoomsBoxList from '../components/RoomsBoxList';
import Room from '../components/Room';
Expand All @@ -12,24 +13,24 @@ import Palettes from '../components/Palettes';
import RoomGfx from '../components/RoomGfx';
import RoomScripts from '../components/RoomScripts';

const RoomsContainer = ({ rooms, roomgfx, globdata }) => {
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,
);

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.
Expand All @@ -40,7 +41,7 @@ const RoomsContainer = ({ rooms, roomgfx, globdata }) => {
selectedObjects[i] = !!(initialState & 0b10000000);
}
setSelectedObjects(selectedObjects);
}, [roomId]);
}, [id]);

const setSelectedObjectState = (id, state) => {
const newSelectedObjects = [...selectedObjects];
Expand Down Expand Up @@ -75,8 +76,12 @@ const RoomsContainer = ({ rooms, roomgfx, globdata }) => {
<>
<PrimaryColumn>
<RoomsList
rooms={rooms}
currentId={currentRoomId}
items={rooms}
currentId={isRoom ? currentId : null}
/>
<TitlesList
items={titles}
currentId={isRoom ? null : currentId}
/>
</PrimaryColumn>
{(room?.objectImages?.length || room?.boxes?.length) && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<canvas
Expand All @@ -39,10 +42,18 @@ const RoomCanvasContainer = ({
);
};

const draw = (ctx, room, baseTiles, roomgfc, selectedObjects) => {
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.
Expand Down Expand Up @@ -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],
Expand All @@ -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;
}

Expand Down Expand Up @@ -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;
Loading

0 comments on commit 5458860

Please sign in to comment.