diff --git a/examples/Makefile b/examples/Makefile index c0f74aa..5561100 100644 --- a/examples/Makefile +++ b/examples/Makefile @@ -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 diff --git a/examples/decodePartyIcons/Makefile b/examples/decodePartyIcons/Makefile new file mode 100644 index 0000000..460f285 --- /dev/null +++ b/examples/decodePartyIcons/Makefile @@ -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) \ No newline at end of file diff --git a/examples/decodePartyIcons/main.cpp b/examples/decodePartyIcons/main.cpp new file mode 100644 index 0000000..d68b3e5 --- /dev/null +++ b/examples/decodePartyIcons/main.cpp @@ -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 +#include +#include + +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 \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; +} \ No newline at end of file diff --git a/examples/decodeSprite/main.cpp b/examples/decodeSprite/main.cpp index 9e80b1d..bd7c479 100644 --- a/examples/decodeSprite/main.cpp +++ b/examples/decodeSprite/main.cpp @@ -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); diff --git a/include/SpriteRenderer.h b/include/SpriteRenderer.h index 82be414..2889efa 100644 --- a/include/SpriteRenderer.h +++ b/include/SpriteRenderer.h @@ -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 \ No newline at end of file diff --git a/include/gen1/Gen1Common.h b/include/gen1/Gen1Common.h index acf5fbd..e8ae4ac 100644 --- a/include/gen1/Gen1Common.h +++ b/include/gen1/Gen1Common.h @@ -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 */ diff --git a/include/gen1/Gen1GameReader.h b/include/gen1/Gen1GameReader.h index 379246d..69b1d95 100644 --- a/include/gen1/Gen1GameReader.h +++ b/include/gen1/Gen1GameReader.h @@ -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 diff --git a/src/SpriteRenderer.cpp b/src/SpriteRenderer.cpp index ca639aa..92bd1e3 100644 --- a/src/SpriteRenderer.cpp +++ b/src/SpriteRenderer.cpp @@ -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_; } \ No newline at end of file diff --git a/src/gen1/Gen1GameReader.cpp b/src/gen1/Gen1GameReader.cpp index 8a7b608..ccd6265 100644 --- a/src/gen1/Gen1GameReader.cpp +++ b/src/gen1/Gen1GameReader.cpp @@ -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();