Add party icon decoding for gen 1 pokémon games.

Now you can decode the pokemon mini sprites/party icons from the rom into an RGB/RGBA buffer
This commit is contained in:
Philippe Symons 2024-09-24 12:21:35 +02:00
parent de5381da81
commit 2643a00ca7
9 changed files with 492 additions and 80 deletions

View File

@ -14,6 +14,7 @@ all:
$(MAKE) -C gen2_removeItem $(MAKECMDGOALS) $(MAKEOVERRIDES)
$(MAKE) -C gen2_getEventFlag $(MAKECMDGOALS) $(MAKEOVERRIDES)
$(MAKE) -C gen2_setEventFlag $(MAKECMDGOALS) $(MAKEOVERRIDES)
$(MAKE) -C decodePartyIcons $(MAKECMDGOALS) $(MAKEOVERRIDES)
clean:
$(MAKE) -C do_stuff_gen1 clean
@ -28,3 +29,4 @@ clean:
$(MAKE) -C gen2_removeItem clean
$(MAKE) -C gen2_getEventFlag clean
$(MAKE) -C gen2_setEventFlag clean
$(MAKE) -C decodePartyIcons clean

View File

@ -0,0 +1,48 @@
# # Compiler flags
CXXFLAGS := -std=c++11 -fno-rtti -fno-exceptions -fno-unwind-tables -Wall -Wextra -I $(CURDIR) -I $(CURDIR)/../../include -g -Os
# Source files directory
SRC_DIR := .
# Build directory
BUILD_DIR := build
# Source files (add more as needed)
SRCS := $(shell find $(SRC_DIR) -type f -name '*.cpp')
# Object files
OBJS := $(patsubst $(SRC_DIR)/%.cpp,$(BUILD_DIR)/%.o,$(SRCS))
LIBS := -lpokemegb
ifeq ($(PNG_SUPPORT),1)
LIBS += -lpng
endif
# Ensure necessary directories exist
# This function ensures the directory for the target exists
define make_directory
@mkdir -p $(dir $@)
endef
# Target executable
TARGET := ../../decodePartyIcons
# Phony targets
.PHONY: all clean
# Default target
all: $(TARGET)
$(TARGET): $(OBJS)
$(CXX) $(CXXFLAGS) $^ -o $@ $(LIBS) -L ../../
# Rule to compile source files
$(BUILD_DIR)/%.o: $(SRC_DIR)/%.cpp | $(BUILD_DIR)
$(make_directory)
$(CXX) $(CXXFLAGS) -c $< -o $@
# Create the build directory if it doesn't exist
$(BUILD_DIR):
mkdir -p $(BUILD_DIR)
# Clean rule
clean:
rm -rf $(BUILD_DIR) $(TARGET)

View File

@ -0,0 +1,87 @@
#include "gen1/Gen1GameReader.h"
#include "gen1/Gen1SpriteDecoder.h"
#include "SpriteRenderer.h"
#include "RomReader.h"
#include "SaveManager.h"
#include "utils.h"
#include <cstdio>
#include <cstring>
#include <cstdlib>
using OutputFormat = SpriteRenderer::OutputFormat;
using TileOrder = SpriteRenderer::TileOrder;
void decodeGen1Icons(IRomReader& romReader, ISaveManager& saveManager, Gen1GameType gen1Type)
{
Gen1GameReader gameReader(romReader, saveManager, gen1Type);
SpriteRenderer renderer;
char fileNameBuf[100];
PokemonIconType iconType;
uint8_t *outputBuffer;
bool firstFrame;
for(int i=0; i < GEN1_ICONTYPE_MAX; ++i)
{
iconType = (PokemonIconType)i;
firstFrame = true;
outputBuffer = gameReader.decodePokemonIcon(iconType, renderer, OutputFormat::RGB, firstFrame);
if(!outputBuffer)
{
fprintf(stderr, "ERROR: Could not decode icon for icon type %d and firstFrame %d!\n", (int)iconType, firstFrame);
continue;
}
snprintf(fileNameBuf, sizeof(fileNameBuf), "%d_frame1.png", i);
write_png(fileNameBuf, outputBuffer, 2 * 8, 2 * 8, false);
firstFrame = false;
outputBuffer = gameReader.decodePokemonIcon(iconType, renderer, OutputFormat::RGB, firstFrame);
if(!outputBuffer)
{
fprintf(stderr, "ERROR: Could not decode icon for type %d and firstFrame %d!\n", (int)iconType, firstFrame);
continue;
}
snprintf(fileNameBuf, sizeof(fileNameBuf), "%d_frame2.png", i);
write_png(fileNameBuf, outputBuffer, 2 * 8, 2 * 8, false);
}
}
int main(int argc, char** argv)
{
if(argc != 2)
{
fprintf(stderr, "Usage: %s <path/to/rom.gbc>\n", argv[0]);
return 1;
}
uint8_t* romBuffer;
uint32_t romFileSize;
romBuffer = readFileIntoBuffer(argv[1], romFileSize);
if(!romBuffer)
{
fprintf(stderr, "ERROR: Couldn't read file %s\n", argv[1]);
return 1;
}
GameboyCartridgeHeader cartridgeHeader;
BufferBasedRomReader romReader(romBuffer, romFileSize);
BufferBasedSaveManager saveManager(nullptr, 0);
readGameboyCartridgeHeader(romReader, cartridgeHeader);
//check if we're dealing with gen 1
const Gen1GameType gen1Type = gen1_determineGameType(cartridgeHeader);
if(gen1Type == Gen1GameType::INVALID)
{
fprintf(stderr, "ERROR: sorry, this tool only supports gen 1!\n");
return 1;
}
decodeGen1Icons(romReader, saveManager, gen1Type);
free(romBuffer);
romBuffer = 0;
return 0;
}

View File

@ -50,7 +50,11 @@ static void decodeGen1Sprite(Gen1GameReader &gameReader, uint8_t pokeIndex)
}
// convert it to the outputFormat
outputBuffer = renderer.draw(spriteBuffer, outputFormat, colorPalette, 7, 7, g_removeBackground);
outputBuffer = renderer.draw(spriteBuffer, outputFormat, colorPalette, 7, 7);
if(g_removeBackground)
{
renderer.removeWhiteBackground(7, 7);
}
snprintf(fileNameBuf, sizeof(fileNameBuf), "%s.png", pokeName);
write_png(fileNameBuf, outputBuffer, SPRITE_BUFFER_WIDTH_IN_PIXELS, SPRITE_BUFFER_HEIGHT_IN_PIXELS, g_removeBackground);
@ -97,7 +101,11 @@ static void decodeGen2Sprite(Gen2GameReader &gameReader, uint8_t pokeIndex)
}
// convert it to the outputFormat
outputBuffer = renderer.draw(spriteBuffer, outputFormat, colorPalette, spriteWidthInTiles, spriteHeightInTiles, g_removeBackground);
outputBuffer = renderer.draw(spriteBuffer, outputFormat, colorPalette, spriteWidthInTiles, spriteHeightInTiles);
if(g_removeBackground)
{
renderer.removeWhiteBackground(spriteWidthInTiles, spriteHeightInTiles);
}
snprintf(fileNameBuf, sizeof(fileNameBuf), "%s.png", pokeName);
write_png(fileNameBuf, outputBuffer, spriteWidthInTiles * 8, spriteHeightInTiles * 8, g_removeBackground);

View File

@ -20,6 +20,17 @@ public:
RGBA32
};
/**
* @brief This enum indicates the order the tiles appear in the input data
*/
enum class TileOrder
{
HORIZONTAL,
VERTICAL
};
SpriteRenderer();
/**
* Returns the number of RGB bytes to store RGB values for a sprite with the given number of horizontal and vertical tiles
*/
@ -28,13 +39,38 @@ public:
/**
* @brief This function draw the given spriteBuffer to the internal rgbBuffer_ and returns this rgbBuffer_ pointer
*
* NOTE: the next call to this function overwrites the data inside the returned buffer. So if you want to keep this data around, copy it.
* NOTE: the next call to this function or drawTile() overwrites the data inside the returned buffer. So if you want to keep this data around, copy it.
* You don't need to free() the data.
*/
uint8_t* draw(const uint8_t* spriteBuffer, OutputFormat format, uint16_t palette[4], uint8_t numHTiles, uint8_t numVTiles, bool removeWhiteBackground = false);
uint8_t* draw(const uint8_t* spriteBuffer, OutputFormat format, uint16_t palette[4], uint8_t numHTiles, uint8_t numVTiles, TileOrder tileOrder = TileOrder::VERTICAL);
/**
* @brief This function draws the given buffer that supposedly contains a tile (16 bytes at 2BPP) to the internal buffer_ at the given [xOffset, yOffset] and
* returns this internal buffer_
*
* NOTE: the next call to this function or draw() overwrites the data inside the returned buffer. So if you want to keep this data around, copy it.
* You don't need to free() the data.
*/
uint8_t* drawTile(const uint8_t* tileBuffer, OutputFormat format, uint16_t palette[4], uint16_t xOffset, uint16_t yOffset, uint8_t outputBufferHTiles, bool mirrorHorizontally = false);
/**
* @brief This function removes the white background in the current internal buffer_ and returns this buffer
*
* NOTE: the next call to draw() or drawTile() overwrites the data inside the returned buffer. So if you want to keep this data around, copy it.
* You don't need to free() the data.
*/
uint8_t* removeWhiteBackground(uint8_t numHTiles, uint8_t numVTiles);
protected:
private:
void internalDrawTile(const uint8_t* tileBuffer, uint16_t xOffset, uint16_t yOffset, uint16_t spriteBufferWidthInPixels, bool mirrorHorizontally);
void setFormatAndPalette(OutputFormat format, uint16_t palette[4]);
uint8_t buffer_[MAX_SPRITE_PIXEL_WIDTH * MAX_SPRITE_PIXEL_HEIGHT * MAX_NUM_BYTES_PER_COLOR_COMPONENT];
OutputFormat format_;
const uint8_t* rgbPalette_;
const uint16_t* rgba16Palette_;
uint8_t numColorComponents_;
};
#endif

View File

@ -87,6 +87,22 @@ enum Gen1PokeType
GEN1_PT_DRAGON = 0x1A
};
enum PokemonIconType
{
GEN1_ICONTYPE_MON = 0,
GEN1_ICONTYPE_BALL,
GEN1_ICONTYPE_HELIX,
GEN1_ICONTYPE_FAIRY,
GEN1_ICONTYPE_BIRD,
GEN1_ICONTYPE_WATER,
GEN1_ICONTYPE_BUG,
GEN1_ICONTYPE_GRASS,
GEN1_ICONTYPE_SNAKE,
GEN1_ICONTYPE_QUADRUPED,
GEN1_ICONTYPE_PIKACHU,
GEN1_ICONTYPE_MAX
};
/**
* @brief Implementation based on https://bulbapedia.bulbagarden.net/wiki/Save_data_structure_(Generation_I)#Checksum
*/

View File

@ -4,6 +4,7 @@
#include "gen1/Gen1SpriteDecoder.h"
#include "gen1/Gen1PlayerPokemonStorage.h"
#include "gen1/Gen1DistributionPokemon.h"
#include "SpriteRenderer.h"
class IRomReader;
class ISaveManager;
@ -113,6 +114,12 @@ public:
*/
uint8_t* decodeSprite(uint8_t bankIndex, uint16_t pointer);
/**
* @brief This function decodes the given pokemon icon and returns the internal buffer of the given SpriteRenderer instance
* The size of the returned buffer is 16x16 pixels
*/
uint8_t* decodePokemonIcon(PokemonIconType iconType, SpriteRenderer& renderer, SpriteRenderer::OutputFormat outputFormat, bool firstFrame = true);
/**
* @brief Adds a pokemon to the save. Tries to add it to the party first. If there's no more room there, it tries to add it to the
* first ingame PC box with open slots

View File

@ -5,6 +5,11 @@
using OutputFormat = SpriteRenderer::OutputFormat;
static const uint8_t TILE_WIDTH = 8;
static const uint8_t TILE_HEIGHT = 8;
static const uint8_t BITS_PER_PIXEL = 2;
static const uint8_t BYTES_PER_TILE = ((TILE_WIDTH * TILE_HEIGHT) / 8) * BITS_PER_PIXEL;
// The next set of functions are defined to remove the white background from a decoded RGBA pokémon sprite by doing edge detection.
// Both RGBA32 and RGBA16 formats are supported
@ -438,116 +443,167 @@ static void removeBackground(uint8_t *buffer, OutputFormat format, int spriteWid
// end of the removeBackground specific code
SpriteRenderer::SpriteRenderer()
: buffer_()
, format_(OutputFormat::RGB)
, rgbPalette_(nullptr)
, rgba16Palette_(nullptr)
, numColorComponents_(0)
{
}
uint32_t SpriteRenderer::getNumRGBBytesFor(uint8_t numHTiles, uint8_t numVTiles) const
{
const uint16_t numPixels = (numHTiles * 8) * (numVTiles * 8);
return numPixels * 3;
}
uint8_t* SpriteRenderer::draw(const uint8_t* spriteBuffer, OutputFormat format, uint16_t palette[4], uint8_t numHTiles, uint8_t numVTiles, bool removeWhiteBackground)
uint8_t* SpriteRenderer::draw(const uint8_t* spriteBuffer, OutputFormat format, uint16_t palette[4], uint8_t numHTiles, uint8_t numVTiles, TileOrder tileOrder)
{
const uint8_t* rgbPalette;
const uint16_t* rgba16Palette;
uint8_t byte0;
uint8_t byte1;
uint16_t x;
uint16_t y;
uint8_t numColorComponents;
// convert -size 56x56 -depth 8 rgb:sprite.rgb output.png
// We're working with a gameboy specific format here:
// every 2 bytes contain the bits for 8 horizontal pixels
// the first of those 2 bytes contain the least significant bits for every of those 8 horizontal pixels
// the second of those 2 bytes contain the most significant bits for every of those 8 horizontal pixels.
// however, the data is stored in column first order: that means that we're walking through the sprite vertically.
// this is a bit confusing, but the gist of it is: you get 8 horizontal pixels (=2 bytes), then you move to the next vertical row.
// then you get another 8 horizontal pixels of that row. And then you move again to the next vertical row.
// you keep doing this until you reach the bottom of the sprite, after which the next column begins. And there you repeat the process
uint32_t i = 0;
uint32_t offset;
uint8_t paletteIndex;
uint8_t bit0;
uint8_t bit1;
// every tile is 8x8
const uint16_t spriteBufferHeightInPixels = numVTiles * 8;
const uint16_t spriteBufferWidthInPixels = numHTiles * 8;
const uint16_t spriteBufferSize = spriteBufferWidthInPixels * spriteBufferHeightInPixels / 8;
const uint32_t spriteBufferSize = (spriteBufferWidthInPixels / 8) * spriteBufferHeightInPixels * BITS_PER_PIXEL;
uint32_t i = 0;
uint16_t x = 0;
uint16_t y = 0;
switch(format)
{
case OutputFormat::RGB:
rgbPalette = convertGBColorPaletteToRGB24(palette);
rgba16Palette = nullptr;
numColorComponents = 3;
break;
case OutputFormat::RGBA32:
rgbPalette = convertGBColorPaletteToRGB24(palette);
rgba16Palette = nullptr;
numColorComponents = 4;
break;
case OutputFormat::RGBA16:
rgbPalette = nullptr;
rgba16Palette = convertGBColorPaletteToRGBA16(palette);
numColorComponents = 2;
break;
default:
return nullptr;
}
setFormatAndPalette(format, palette);
while(i < spriteBufferSize)
{
byte0 = spriteBuffer[i * 2];
byte1 = spriteBuffer[i * 2 + 1];
internalDrawTile(spriteBuffer + i, x, y, spriteBufferWidthInPixels, false);
// printf("0x%hhu 0x%hhu\n", byte0, byte1);
x = (i / (spriteBufferHeightInPixels)) * 8;
y = (i % (spriteBufferHeightInPixels));
for(uint8_t bitIndex = 0; bitIndex < 8; ++bitIndex)
if(tileOrder == TileOrder::VERTICAL)
{
offset = (y * spriteBufferWidthInPixels + x + bitIndex) * numColorComponents;
y += 8;
if(y >= spriteBufferHeightInPixels)
{
x += 8;
y = 0;
}
}
else
{
x += 8;
if(x >= spriteBufferWidthInPixels)
{
x = 0;
y += 8;
}
}
bit0 = (byte0 >> (7 - bitIndex)) & 0x1;
bit1 = (byte1 >> (7 - bitIndex)) & 0x1;
i += BYTES_PER_TILE;
}
return buffer_;
}
uint8_t* SpriteRenderer::drawTile(const uint8_t* tileBuffer, OutputFormat format, uint16_t palette[4], uint16_t xOffset, uint16_t yOffset, uint8_t outputBufferHTiles, bool mirrorHorizontally)
{
const uint16_t spriteBufferWidthInPixels = outputBufferHTiles * 8;
setFormatAndPalette(format, palette);
internalDrawTile(tileBuffer, xOffset, yOffset, spriteBufferWidthInPixels, mirrorHorizontally);
return buffer_;
}
uint8_t* SpriteRenderer::removeWhiteBackground(uint8_t numHTiles, uint8_t numVTiles)
{
if(format_ == OutputFormat::RGB)
{
fprintf(stderr, "[SpriteRenderer]: %s: ERROR: removeBackground is not supported for OutputFormat::RGB!", __FUNCTION__);
return buffer_;
}
const uint16_t spriteBufferHeightInPixels = numVTiles * 8;
const uint16_t spriteBufferWidthInPixels = numHTiles * 8;
removeBackground(buffer_, format_, spriteBufferWidthInPixels, spriteBufferHeightInPixels);
return buffer_;
}
void SpriteRenderer::internalDrawTile(const uint8_t* tileBuffer, uint16_t xOffset, uint16_t yOffset, uint16_t spriteBufferWidthInPixels, bool mirrorHorizontally)
{
uint32_t offset;
uint32_t i = 0;
uint16_t x;
uint16_t y = 0;
uint8_t byte0;
uint8_t byte1;
uint8_t bit0;
uint8_t bit1;
uint8_t paletteIndex;
while(i < BYTES_PER_TILE)
{
byte0 = tileBuffer[i];
byte1 = tileBuffer[i + 1];
for(x = 0; x < 8; ++x)
{
offset = ((yOffset + y) * spriteBufferWidthInPixels + xOffset + x) * numColorComponents_;
if(!mirrorHorizontally)
{
bit0 = (byte0 >> (7 - x)) & 0x1;
bit1 = (byte1 >> (7 - x)) & 0x1;
}
else
{
bit0 = (byte0 >> x) & 0x1;
bit1 = (byte1 >> x) & 0x1;
}
paletteIndex = (bit1 << 1) | bit0;
if(format == OutputFormat::RGBA16)
if(format_ == OutputFormat::RGBA16)
{
(*((uint16_t*)(buffer_ + offset))) = rgba16Palette[paletteIndex];
(*((uint16_t*)(buffer_ + offset))) = rgba16Palette_[paletteIndex];
}
else
{
// R
buffer_[offset] = rgbPalette[paletteIndex * 3 + 0];
buffer_[offset] = rgbPalette_[paletteIndex * 3 + 0];
// G
buffer_[offset + 1] = rgbPalette[paletteIndex * 3 + 1];
buffer_[offset + 1] = rgbPalette_[paletteIndex * 3 + 1];
// B
buffer_[offset + 2] = rgbPalette[paletteIndex * 3 + 2];
buffer_[offset + 2] = rgbPalette_[paletteIndex * 3 + 2];
if(format == OutputFormat::RGBA32)
if(format_ == OutputFormat::RGBA32)
{
// A
buffer_[offset + 3] = 0xFF;
}
}
}
++i;
}
++y;
if(removeWhiteBackground)
i += 2;
}
}
void SpriteRenderer::setFormatAndPalette(OutputFormat format, uint16_t palette[4])
{
format_ = format;
switch(format)
{
if(format == OutputFormat::RGB)
{
fprintf(stderr, "[SpriteRenderer]: %s: ERROR: removeBackground is not supported for OutputFormat::RGB!", __FUNCTION__);
return buffer_;
}
removeBackground(buffer_, format, spriteBufferWidthInPixels, spriteBufferHeightInPixels);
case OutputFormat::RGB:
rgbPalette_ = convertGBColorPaletteToRGB24(palette);
rgba16Palette_ = nullptr;
numColorComponents_ = 3;
break;
case OutputFormat::RGBA32:
rgbPalette_ = convertGBColorPaletteToRGB24(palette);
rgba16Palette_ = nullptr;
numColorComponents_ = 4;
break;
case OutputFormat::RGBA16:
rgbPalette_ = nullptr;
rgba16Palette_ = convertGBColorPaletteToRGBA16(palette);
numColorComponents_ = 2;
break;
default:
break;
}
return buffer_;
}

View File

@ -419,6 +419,158 @@ uint8_t* Gen1GameReader::decodeSprite(uint8_t bankIndex, uint16_t pointer)
return spriteDecoder_.decode(bankIndex, pointer);
}
/*
* In gen 1, the behaviour of the party icons is a bit messy.
* There's the MonPartySpritePointers of the various icon sprites. But while some of these sprites are stored fully,
* for some only the left half is stored. (and the code is supposed to mirror this half while rendering)
* In fact, for the latter, the top and bottom left tiles are separate entries in the table. This is not the case for the icons that are
* stored fully.
*
* There's also no real flag to indicate this.
*
* 2 icons don't even have a second frame stored. As a matter of fact, the HELIX icon doesn't even have an entry in the table at all!
*
* Anyway, we need to deal with this mess and that makes our code below a bit messy as well.
*/
uint8_t* Gen1GameReader::decodePokemonIcon(PokemonIconType iconType, SpriteRenderer& renderer, SpriteRenderer::OutputFormat outputFormat, bool firstFrame)
{
const uint32_t romOffset = (gameType_ == Gen1GameType::YELLOW) ? 0x7184D : 0x717C0;
const uint8_t MAX_NUM_TILES = 8;
const uint8_t TILE_WIDTH = 8;
const uint8_t TILE_HEIGHT = 8;
const uint8_t BITS_PER_PIXEL = 2;
const uint8_t BYTES_PER_TILE = ((TILE_WIDTH * TILE_HEIGHT) / 8) * BITS_PER_PIXEL;
const uint32_t ENTRY_SIZE = 6;
uint8_t iconSrcBuffer[MAX_NUM_TILES * BYTES_PER_TILE];
uint16_t pointer;
uint8_t numTiles; // total number of tiles of the sprite (to indicate byte size)
uint8_t bankIndex;
uint16_t vSpritesTileOffset;
uint8_t entryIndex;
// this boolean indicates that the sprite only stores the left half, but it's symmetric
bool isSymmetric;
// TODO: for some of these icons, only the left half is stored.
// they are supposed to be mirrored
switch(iconType)
{
case GEN1_ICONTYPE_MON:
entryIndex = (firstFrame) ? 14 : 0;
isSymmetric = false;
break;
case GEN1_ICONTYPE_BALL:
entryIndex = (firstFrame) ? 1 : 0xFF;
isSymmetric = false;
break;
case GEN1_ICONTYPE_FAIRY:
entryIndex = (firstFrame) ? 16 : 2;
isSymmetric = false;
break;
case GEN1_ICONTYPE_BIRD:
entryIndex = (firstFrame) ? 17 : 3;
isSymmetric = false;
break;
case GEN1_ICONTYPE_WATER:
entryIndex = (firstFrame) ? 4 : 18;
isSymmetric = false;
break;
case GEN1_ICONTYPE_BUG:
entryIndex = (firstFrame) ? 19 : 5;
isSymmetric = true;
break;
case GEN1_ICONTYPE_GRASS:
entryIndex = (firstFrame) ? 21 : 7;
isSymmetric = true;
break;
case GEN1_ICONTYPE_SNAKE:
entryIndex = (firstFrame) ? 23 : 9;
isSymmetric = true;
break;
case GEN1_ICONTYPE_QUADRUPED:
entryIndex = (firstFrame) ? 25 : 11;
isSymmetric = true;
break;
case GEN1_ICONTYPE_PIKACHU:
{
if(gameType_ != Gen1GameType::YELLOW)
{
entryIndex = 0xFF;
}
else
{
entryIndex = (firstFrame) ? 13 : 27;
}
isSymmetric = false;
break;
}
default:
entryIndex = 0xFF;
isSymmetric = false;
break;
}
// HACK: In pokemon yellow, there's an additional PIKACHU entry in the MonPartySpritePointers table.
// This shifts every entryIndex by 1 after this new entry.
// In practice, this means that mostly the second frame of icons are affected.
// so we need to increase the entryIndex by one in this case.
if(gameType_ == Gen1GameType::YELLOW && entryIndex != 0xFF && entryIndex > 13)
{
++entryIndex;
}
if(entryIndex == 0xFF)
{
if(iconType == PokemonIconType::GEN1_ICONTYPE_HELIX && firstFrame)
{
// The helix sprite is not part of the MonPartySpritePointers table for some reason
bankIndex = 4;
pointer = (gameType_ == Gen1GameType::YELLOW) ? 0x7525 : 0x5180;
numTiles = 4;
}
else
{
return nullptr;
}
}
else
{
romReader_.seek(romOffset);
romReader_.advance(ENTRY_SIZE * entryIndex);
romReader_.readUint16(pointer);
romReader_.readByte(numTiles);
romReader_.readByte(bankIndex);
romReader_.readUint16(vSpritesTileOffset);
if(numTiles == 1 && isSymmetric)
{
numTiles = 2;
}
}
romReader_.seekToRomPointer(pointer, bankIndex);
romReader_.read(iconSrcBuffer, BYTES_PER_TILE * numTiles);
if(isSymmetric)
{
const uint8_t* srcTop = iconSrcBuffer;
const uint8_t* srcBottom = iconSrcBuffer + BYTES_PER_TILE;
// top left corner
renderer.drawTile(srcTop, outputFormat, monochromeGBColorPalette, 0, 0, 2, false);
// top right corner (mirrored)
renderer.drawTile(srcTop, outputFormat, monochromeGBColorPalette, TILE_WIDTH, 0, 2, true);
// bottom left corner
renderer.drawTile(srcBottom, outputFormat, monochromeGBColorPalette, 0, TILE_HEIGHT, 2, false);
// bottom right corner (mirrored)
return renderer.drawTile(srcBottom, outputFormat, monochromeGBColorPalette, TILE_WIDTH, TILE_HEIGHT, 2, true);
}
else
{
return renderer.draw(iconSrcBuffer, outputFormat, monochromeGBColorPalette, 2, 2, SpriteRenderer::TileOrder::HORIZONTAL);
}
}
uint8_t Gen1GameReader::addPokemon(Gen1TrainerPokemon& poke, const char* originalTrainerID, const char* nickname)
{
Gen1Party party = getParty();