-
Notifications
You must be signed in to change notification settings - Fork 8
Development Guide
For anyone thinking of working on Smaghetti's codebase, this doc walks through everything there is to know.
You need
- Node (I am using 12 at time of writing, any new/recent Node should be fine)
- yarn 1, I am using 1.22.17. yarn 2, 3 and npm will all cause issues
clone the repo to your local machine then yarn
to install dependencies
yarn develop
will then start a local server on localhost:3000
, it will also build storybook and start it on localhost:6006
, it should pop open a browser to storybook once it is ready.
The app is a pretty typical nextjs app using redux and tailwind. src/pages
is where all the pages are defined. I never put much code or logic there, these pages always call out to an actual page implementation. For example src/pages/editor/index.tsx
basically just renders MakePage
, which is defined over in src/components/editor/MakePage
Smaghetti always uses the containers/dummy approach but is admittedly a bit loose here. A decent example of a dumb component is DownloadButton and its corresponding container ConnectedDownloadButton
DownloadButton
just renders DOM and has no logic. ConnectedDownloadButton
is the opposite, no DOM and all logic. Both are placed in a DownloadButton/
folder where index.ts
does export { ConnectedDownloadButton as DownloadButton }
, so the rest of the app doesn't really know or care about connected vs dummy, the components are just black boxes.
The src/entities
directory is where all entity code lives. An entity is something in SMA4 such as a Goomba, Brick, etc. The goal is to keep the entity source of truth entirely in one file (or directory) as much as possible. For example, Goomba.tsx contains everything that is needed to work with goombas:
- drawing them in the editor
- getting their binary data for adding into a SMA4 save file
- their resource and palette info for extracting out their image for display
- what object set and sprite graphic sets they are compatible with
- where they get placed in the palette (aka "item chooser")
Basically if you need to work on Goomba, it is found in this file. This has proven to work really well. It's very easy to say fix a Goomba bug, or add a new entity.
Entities are a tad strange in that their simpleRender and render functions return JSX. So they are kind of like React components with a bunch of extra "stuff". This strangeness has paid off though. Originally I was trying to handle details panes and all of the various things entities need in a detached, generic way. But it got so overwhelming and out of control. Entities in SMA4 behave in so many different ways, that just letting the Entity types handle their own rendering, their own detail panes, etc, has proven to be far more successful. I still extract out commonalities where it makes sense, such as GeneratorFrame which all generator entities use.
Smaghetti uses supabase as its backend. There is no local backend, and currently you must hook up your dev environment to a supabase backend. I will work to eliminate that.
Setting up a supabase environment involves creating one in supabase, then with the db credentials, running all of the migrations. supabase.md shows how to do that.
All supabase calls are found in remoteData
There really isn't too much, basically just save levels, delete levels, vote on levels, and display published levels on the levels page. All in all, the backend is a minor part of Smaghetti.
Smaghetti does not use static image files for entities. Instead, images are generated from the ROM itself. The first time someone comes to Smaghetti's editor, it will take a good while to extract all the images. But these images are saved locally in the browser, so on subsequent visits only new and changed images need to get generated.
Both tiles and palettes are needed for an entity's images to get extracted
For small entities, you can get their tiles manually. Let's pretend we are adding GreenKoopaTroopa as a new entity to the editor:
Edit Goomba.tsx
so that its toSpriteBinary
emits a koopa troopa instead of a goomba.
So change it from this
toSpriteBinary({ x, y }) {
return [0, this.objectId, x, y];
}
to this
toSpriteBinary({ x, y }) {
return [0, 0x6c, x, y];
}
Where 6c
is green koopa troopa's object id.
Now in the editor, add a goomba to a level and play the level. When playing the level it will actually be a koopa due to the hack we just did.
Download this level and set it to be your mGBA save file for your sma4 rom.
Start up mGBA, load your level, pause the emulator (ctrl+p), then pull up the sprite viewer
Highlight one of the koopa's sprites, then one of the sprite's tiles. You are interested in the tile's vram address, and later you will also want the palette number (both boxed in red above).
Now pull up mGBA's memory viewer, and go to the vram address you found above, highlight the two rows, right click, and choose "copy selection"
The tile's data is now in your clipboard. Now head to the tile viewer click "dump compressed", then click "show indices", then in the far right text box, enter in the tile data you copied from mGBA and click "search". You will be taken to this tile's location in the ROM
From here you now have all the info you need to define the koopa's tiles in its entity file. You need the offset at the top, in this case 0x134104
, and all of the tile indices that make up the Koopa. Go ahead and dump them into the koopa's resource
in its entity, like this:
resource: {
palettes: [
[],
],
romOffset: 0x134104,
tiles: [
[258, 259],
[290, 291],
[322, 323],
[354, 355],
],
},
Now the koopa's tiles are defined. Now let's grab the palette.
In the sprite viewer we saw we needed palette 2. So in the memory viewer, set the dropdown to palette memory. There are 32 palettes in total. The first 16 are for backgrounds, and the last 16 are for sprites (called objects in GBA terminology, which is confusing for SMA4).
Since this sprite uses palette 2, we actually want palette 18 (since this is a sprite and not a background, skip the first 16 palettes). This is a zero based index, so it is actually the nineteenth palette. Find the nineteenth palette (each palette is two rows), highlight it, right click and choose "copy selection". Now the palette data is in your clipboard
Head to a terminal in the smaghetti directory and run scripts/mgbaPaletteToArray.ts
, using the copied palette data as the argument
12:09 $ yarn run-ts-node scripts/mgbaPaletteToArray.ts 967FFF7FC6186B02101BB413FD259E365F47BF1A1C003F253F46D17A2C6EA659
yarn run v1.22.17
$ yarn ts-node --compiler-options '{ "module": "commonjs", "jsx": "react" }' scripts/mgbaPaletteToArray.ts 967FFF7FC6186B02101BB413FD259E365F47BF1A1C003F253F46D17A2C6EA659
$ /home/matt/dev/smaghetti/node_modules/.bin/ts-node --compiler-options '{ "module": "commonjs", "jsx": "react" }' scripts/mgbaPaletteToArray.ts 967FFF7FC6186B02101BB413FD259E365F47BF1A1C003F253F46D17A2C6EA659
[ 0x7f96, 0x7fff, 0x18c6, 0x26b, 0x1b10, 0x13b4, 0x25fd, 0x369e, 0x475f, 0x1abf, 0x1c, 0x253f, 0x463f, 0x7ad1, 0x6e2c, 0x59a6 ]
Done in 2.00s.
The line just above "Done" is the palette data converted into a JavaScript array. Just copy it into the resource:
resource: {
palettes: [
[
0x7f96,
0x7fff,
0x18c6,
0x26b,
0x1b10,
0x13b4,
0x25fd,
0x369e,
0x475f,
0x1abf,
0x1c,
0x253f,
0x463f,
0x7ad1,
0x6e2c,
0x59a6,
],
],
romOffset: 0x134104,
tiles: [
[258, 259],
[290, 291],
[322, 323],
[354, 355],
],
}
Smaghetti can now take this data and generate the koopa's image for use in the editor.
Smaller entities like a koopa only have one palette. More complex entities often have multiple palettes. Add each palette into the palettes array, then in the tiles array, a tile can indicate which palette it uses. The default is zero (the first palette). For example, here is ParaBobombGenerator
resource: {
romOffset: 0x18c914,
palettes: [
[
0x7f96,
0x7fff,
0x18c6,
0x26b,
0x1b10,
0x13b4,
0x25fd,
0x369e,
0x475f,
0x1abf,
0x1c,
0x253f,
0x463f,
0x7ad1,
0x6e2c,
0x59a6,
],
[
0x7fb4,
0x7fff,
0x0,
0x75ad,
0x7a94,
0x7f39,
0x25de,
0x273f,
0x1b1d,
0x2fbf,
0x53ff,
0x119,
0x167b,
0x6ab2,
0x7b98,
0x7bdd,
],
],
tiles: [
[
{ tileIndex: 64, palette: 1 },
{ tileIndex: 65, palette: 1 },
],
[
{ tileIndex: 96, palette: 1 },
{ tileIndex: 97, palette: 1 },
],
[69, { tileIndex: 69, flip: 'h' }],
[101, { tileIndex: 101, flip: 'h' }],
],
},
Tiles can also be flipped either vertically or horizontally, above two tiles are flipped horizontally, flip: 'h'
The above approach is fine for small entities. It seems a bit tedious but you can get a small entity's resource in just a minute or two. But large resources are another story. Backgrounds are the worst offender here. Getting a background using the manual approach is an absolute nightmare. So instead, you can use the scripts/png2res.ts
script. You can use png2res for small entities too.
Let's pretend the winter background isn't in Smaghetti yet and we'll add it.
Similar to how you hacked the goomba to emit a koopa, hack an existing background to emit the winter background. Head to levelData/constants and change plains
from 0x5
to 0x8
. 0x8
is the winter background value.
In the editor, create a level with the plains background, then save it and load it into mGBA. It will actually be the winter background since we hacked the value. Pause the emulator (ctrl+p) and pull up the map viewer, then select the correct background layer
In the lower left, click "export" and save the png somewhere. Load it into an image editor and crop it down as needed. For example, here is how I cropped the winter bg
Now open up the memory viewer, go to palette memory, and select all of the palette memory
Now click "save selection", and save it somewhere
Now run png2res
12:26 $ TS_NODE_FILES=true yarn run-ts-node scripts/png2res ../sma4/sma4.gba pngsForResources/winterWiki.png pngsForResources/winterWiki.palette > winterBg.ts
Redirect the output to a file, and then that file will have your resource in it (plus some additional output junk you need to trim)
yarn run v1.22.17
$ yarn ts-node --compiler-options '{ "module": "commonjs", "jsx": "react" }' scripts/png2res ../sma4/sma4.gba pngsForResources/winterWiki.png pngsForResources/winterWiki.palette
$ /home/matt/dev/smaghetti/node_modules/.bin/ts-node --compiler-options '{ "module": "commonjs", "jsx": "react" }' scripts/png2res ../sma4/sma4.gba pngsForResources/winterWiki.png pngsForResources/winterWiki.palette
// generated by png2res
{
"palettes": [
[
32662,
...
28439,
30585,
24373,
23316,
21211,
18007
],
...
],
"tiles": [
[
{
"tileIndex": 144,
"palette": 1
},
{
"tileIndex": 144,
"palette": 1
},
...
Sometimes png2res fails to figure out a tile and there will be null
s. This is due to how the lossy conversion from 24bit to 16bit color space works. I've yet to find a good solution to this. But thankfully, nulls are rare and if they do happen, it's usually not too hard to manually patch the resource using the manual method.