diff --git a/examples/decodePartyIcons/main.cpp b/examples/decodePartyIcons/main.cpp index b177dac..acb36d3 100644 --- a/examples/decodePartyIcons/main.cpp +++ b/examples/decodePartyIcons/main.cpp @@ -10,21 +10,22 @@ #include using OutputFormat = SpriteRenderer::OutputFormat; -using TileOrder = SpriteRenderer::TileOrder; static void decodeGen1Icon(IRomReader& romReader, ISaveManager& saveManager, Gen1GameType gen1Type, Gen1PokemonIconType iconType, bool firstFrame) { Gen1GameReader gameReader(romReader, saveManager, gen1Type); SpriteRenderer renderer; char fileNameBuf[100]; - uint8_t *outputBuffer; + uint8_t* spriteBuffer; + uint8_t* outputBuffer; - outputBuffer = gameReader.decodePokemonIcon(iconType, renderer, OutputFormat::RGB, firstFrame); - if(!outputBuffer) + spriteBuffer = gameReader.decodePokemonIcon(iconType, firstFrame); + if(!spriteBuffer) { fprintf(stderr, "ERROR: Could not decode icon for icon type %d and firstFrame %d!\n", (int)iconType, firstFrame); return; } + outputBuffer = renderer.draw(spriteBuffer, OutputFormat::RGB, monochromeGBColorPalette, GEN1_ICON_WIDTH_IN_TILES, GEN1_ICON_HEIGHT_IN_TILES); snprintf(fileNameBuf, sizeof(fileNameBuf), "%d_frame%u.png", (int)iconType, (firstFrame) ? 1 : 2); write_png(fileNameBuf, outputBuffer, 2 * 8, 2 * 8, false); } @@ -34,14 +35,16 @@ static void decodeGen2Icon(IRomReader& romReader, ISaveManager& saveManager, Gen Gen2GameReader gameReader(romReader, saveManager, gen2Type); SpriteRenderer renderer; char fileNameBuf[100]; - uint8_t *outputBuffer; + uint8_t* spriteBuffer; + uint8_t* outputBuffer; - outputBuffer = gameReader.decodePokemonIcon(iconType, renderer, OutputFormat::RGB, firstFrame); - if(!outputBuffer) + spriteBuffer = gameReader.decodePokemonIcon(iconType, firstFrame); + if(!spriteBuffer) { fprintf(stderr, "ERROR: Could not decode icon for icon type %d and firstFrame %d!\n", (int)iconType, firstFrame); return; } + outputBuffer = renderer.draw(spriteBuffer, OutputFormat::RGB, gen2_iconColorPalette, GEN2_ICON_WIDTH_IN_TILES, GEN2_ICON_HEIGHT_IN_TILES); snprintf(fileNameBuf, sizeof(fileNameBuf), "%d_frame%u.png", (int)iconType, (firstFrame) ? 1 : 2); write_png(fileNameBuf, outputBuffer, 2 * 8, 2 * 8, false); } diff --git a/include/SpriteRenderer.h b/include/SpriteRenderer.h index 2889efa..a331474 100644 --- a/include/SpriteRenderer.h +++ b/include/SpriteRenderer.h @@ -20,15 +20,6 @@ public: RGBA32 }; - /** - * @brief This enum indicates the order the tiles appear in the input data - */ - enum class TileOrder - { - HORIZONTAL, - VERTICAL - }; - SpriteRenderer(); /** @@ -42,16 +33,7 @@ public: * 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, 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); + uint8_t* draw(const uint8_t* spriteBuffer, OutputFormat format, uint16_t palette[4], uint8_t numHTiles, uint8_t numVTiles); /** * @brief This function removes the white background in the current internal buffer_ and returns this buffer @@ -62,7 +44,7 @@ public: 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 internalDrawTile(const uint8_t* tileBuffer, uint16_t xOffset, uint16_t yOffset, uint16_t spriteBufferWidthInPixels); void setFormatAndPalette(OutputFormat format, uint16_t palette[4]); diff --git a/include/gen1/Gen1GameReader.h b/include/gen1/Gen1GameReader.h index 6bf6471..667f4fd 100644 --- a/include/gen1/Gen1GameReader.h +++ b/include/gen1/Gen1GameReader.h @@ -2,9 +2,9 @@ #define GEN1GAMEREADER_H #include "gen1/Gen1SpriteDecoder.h" +#include "gen1/Gen1IconDecoder.h" #include "gen1/Gen1PlayerPokemonStorage.h" #include "gen1/Gen1DistributionPokemon.h" -#include "SpriteRenderer.h" class IRomReader; class ISaveManager; @@ -120,10 +120,14 @@ 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 + * @brief This function decodes the given pokemon icon and returns an internal buffer + * Note that this returns a 16x16 buffer in gameboy format with tiles in vertical order. + * You need to feed it to an instance of SpriteRenderer to convert it to a useful format + * + * WARNING: this function returns a buffer to an internal buffer of Gen1IconDecoder. That means it will get overwritten on the next call to this function. + * If you want to keep the content around for longer, make a copy of this data */ - uint8_t* decodePokemonIcon(Gen1PokemonIconType iconType, SpriteRenderer& renderer, SpriteRenderer::OutputFormat outputFormat, bool firstFrame = true); + uint8_t* decodePokemonIcon(Gen1PokemonIconType iconType, 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 @@ -180,6 +184,7 @@ private: IRomReader& romReader_; ISaveManager& saveManager_; Gen1SpriteDecoder spriteDecoder_; + Gen1IconDecoder iconDecoder_; Gen1GameType gameType_; }; diff --git a/include/gen1/Gen1IconDecoder.h b/include/gen1/Gen1IconDecoder.h new file mode 100644 index 0000000..3096e91 --- /dev/null +++ b/include/gen1/Gen1IconDecoder.h @@ -0,0 +1,38 @@ +#ifndef _GEN1POKEMONICONDECODER_H +#define _GEN1POKEMONICONDECODER_H + +#include "gen1/Gen1Common.h" + +#define GEN1_TILE_BITS_PER_PIXEL 2 +#define GEN1_BYTES_PER_TILE 16 +#define GEN1_ICON_WIDTH_IN_TILES 2 +#define GEN1_ICON_HEIGHT_IN_TILES 2 +#define GEN1_ICON_NUM_BYTES GEN1_BYTES_PER_TILE * 4 + +class IRomReader; + +/** + * @brief This class is responsible for decoding pokemon party menu icons for Gen 1 games + */ +class Gen1IconDecoder +{ +public: + Gen1IconDecoder(IRomReader& romReader, Gen1GameType gameType); + + /** + * @brief Decodes the specified icon type. either frame 1 or frame 2. + * This function returns a buffer containing gameboy tiles. + * + * The result is supposed to be fed to an instance of the SpriteRenderer class to convert it into an actual usable format + * + * @return Internal buffer containing the icon tiles in vertical tile order. + */ + uint8_t* decode(Gen1PokemonIconType iconType, bool firstFrame); +protected: +private: + uint8_t buffer_[GEN1_ICON_NUM_BYTES]; + IRomReader& romReader_; + Gen1GameType gameType_; +}; + +#endif \ No newline at end of file diff --git a/include/gen2/Gen2GameReader.h b/include/gen2/Gen2GameReader.h index 1287d8b..71cedf1 100644 --- a/include/gen2/Gen2GameReader.h +++ b/include/gen2/Gen2GameReader.h @@ -2,9 +2,9 @@ #define _GEN2GAMEREADER_H #include "gen2/Gen2SpriteDecoder.h" +#include "gen2/Gen2IconDecoder.h" #include "gen2/Gen2PlayerPokemonStorage.h" #include "gen2/Gen2DistributionPokemon.h" -#include "SpriteRenderer.h" class IRomReader; class ISaveManager; @@ -75,10 +75,14 @@ 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 + * @brief This function decodes the given pokemon icon and returns an internal buffer + * Note that this returns a 16x16 buffer in gameboy format with tiles in vertical order. + * You need to feed it to an instance of SpriteRenderer to convert it to a useful format + * + * WARNING: this function returns a buffer to an internal buffer of Gen2IconDecoder. That means it will get overwritten on the next call to this function. + * If you want to keep the content around for longer, make a copy of this data */ - uint8_t* decodePokemonIcon(Gen2PokemonIconType iconType, SpriteRenderer& renderer, SpriteRenderer::OutputFormat outputFormat, bool firstFrame = true); + uint8_t* decodePokemonIcon(Gen2PokemonIconType iconType, 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 @@ -227,6 +231,7 @@ private: IRomReader &romReader_; ISaveManager &saveManager_; Gen2SpriteDecoder spriteDecoder_; + Gen2IconDecoder iconDecoder_; Gen2GameType gameType_; }; diff --git a/include/gen2/Gen2IconDecoder.h b/include/gen2/Gen2IconDecoder.h new file mode 100644 index 0000000..8e15f8e --- /dev/null +++ b/include/gen2/Gen2IconDecoder.h @@ -0,0 +1,38 @@ +#ifndef _GEN2ICONDECODER_H +#define _GEN2ICONDECODER_H + +#include "gen2/Gen2Common.h" + +#define GEN2_TILE_BITS_PER_PIXEL 2 +#define GEN2_BYTES_PER_TILE 16 +#define GEN2_ICON_WIDTH_IN_TILES 2 +#define GEN2_ICON_HEIGHT_IN_TILES 2 +#define GEN2_ICON_NUM_BYTES GEN2_BYTES_PER_TILE * 4 + +class IRomReader; + +/** + * @brief This class is responsible for decoding pokemon party menu icons for Gen 2 games + */ +class Gen2IconDecoder +{ +public: + Gen2IconDecoder(IRomReader& romReader, Gen2GameType gameType); + + /** + * @brief Decodes the specified icon type. either frame 1 or frame 2. + * This function returns a buffer containing gameboy tiles. + * + * The result is supposed to be fed to an instance of the SpriteRenderer class to convert it into an actual usable format + * + * @return Internal buffer containing the icon tiles in vertical tile order. + */ + uint8_t* decode(Gen2PokemonIconType iconType, bool firstFrame); +protected: +private: + uint8_t buffer_[GEN2_ICON_NUM_BYTES]; + IRomReader& romReader_; + Gen2GameType gameType_; +}; + +#endif \ No newline at end of file diff --git a/src/SpriteRenderer.cpp b/src/SpriteRenderer.cpp index 92bd1e3..6fcd027 100644 --- a/src/SpriteRenderer.cpp +++ b/src/SpriteRenderer.cpp @@ -458,7 +458,7 @@ uint32_t SpriteRenderer::getNumRGBBytesFor(uint8_t numHTiles, uint8_t numVTiles) return numPixels * 3; } -uint8_t* SpriteRenderer::draw(const uint8_t* spriteBuffer, OutputFormat format, uint16_t palette[4], uint8_t numHTiles, uint8_t numVTiles, TileOrder tileOrder) +uint8_t* SpriteRenderer::draw(const uint8_t* spriteBuffer, OutputFormat format, uint16_t palette[4], uint8_t numHTiles, uint8_t numVTiles) { const uint16_t spriteBufferHeightInPixels = numVTiles * 8; const uint16_t spriteBufferWidthInPixels = numHTiles * 8; @@ -471,25 +471,13 @@ uint8_t* SpriteRenderer::draw(const uint8_t* spriteBuffer, OutputFormat format, while(i < spriteBufferSize) { - internalDrawTile(spriteBuffer + i, x, y, spriteBufferWidthInPixels, false); + internalDrawTile(spriteBuffer + i, x, y, spriteBufferWidthInPixels); - if(tileOrder == TileOrder::VERTICAL) - { - y += 8; - if(y >= spriteBufferHeightInPixels) - { - x += 8; - y = 0; - } - } - else + y += 8; + if(y >= spriteBufferHeightInPixels) { x += 8; - if(x >= spriteBufferWidthInPixels) - { - x = 0; - y += 8; - } + y = 0; } i += BYTES_PER_TILE; @@ -498,16 +486,6 @@ uint8_t* SpriteRenderer::draw(const uint8_t* spriteBuffer, OutputFormat format, 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) @@ -523,7 +501,7 @@ uint8_t* SpriteRenderer::removeWhiteBackground(uint8_t numHTiles, uint8_t numVTi return buffer_; } -void SpriteRenderer::internalDrawTile(const uint8_t* tileBuffer, uint16_t xOffset, uint16_t yOffset, uint16_t spriteBufferWidthInPixels, bool mirrorHorizontally) +void SpriteRenderer::internalDrawTile(const uint8_t* tileBuffer, uint16_t xOffset, uint16_t yOffset, uint16_t spriteBufferWidthInPixels) { uint32_t offset; uint32_t i = 0; @@ -544,16 +522,8 @@ void SpriteRenderer::internalDrawTile(const uint8_t* tileBuffer, uint16_t xOffse { 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; - } + bit0 = (byte0 >> (7 - x)) & 0x1; + bit1 = (byte1 >> (7 - x)) & 0x1; paletteIndex = (bit1 << 1) | bit0; diff --git a/src/gen1/Gen1GameReader.cpp b/src/gen1/Gen1GameReader.cpp index 59d8c9a..9e0239f 100644 --- a/src/gen1/Gen1GameReader.cpp +++ b/src/gen1/Gen1GameReader.cpp @@ -51,6 +51,7 @@ Gen1GameReader::Gen1GameReader(IRomReader &romReader, ISaveManager &saveManager, : romReader_(romReader) , saveManager_(saveManager) , spriteDecoder_(romReader_) + , iconDecoder_(romReader, gameType) , gameType_(gameType) { } @@ -455,159 +456,9 @@ 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(Gen1PokemonIconType iconType, SpriteRenderer& renderer, SpriteRenderer::OutputFormat outputFormat, bool firstFrame) +uint8_t* Gen1GameReader::decodePokemonIcon(Gen1PokemonIconType iconType, 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; - - 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 == Gen1PokemonIconType::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); - - // HACK: so, in the case that only the left half of the icon is stored, every tile (top left + bottom left) - // gets a separate entry in the table. But that's a bit annoying to work with. Since both tiles are stored - // contiguously on the cartridge, let's just manipulate the numTiles field because we're going to render the tiles - // separately anyway because we're aware that we're in this scenario. - if(numTiles == 1 && isSymmetric) - { - numTiles = 2; - } - } - - romReader_.seekToRomPointer(pointer, bankIndex); - romReader_.read(iconSrcBuffer, BYTES_PER_TILE * numTiles); - - if(isSymmetric) - { - // only the left half is stored. We're supposed to mirror it to get the right half. - 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 - { - // full sprite is stored in a horizontal tile order. So we can decode them this way. - return renderer.draw(iconSrcBuffer, outputFormat, monochromeGBColorPalette, 2, 2, SpriteRenderer::TileOrder::HORIZONTAL); - } + return iconDecoder_.decode(iconType, firstFrame); } uint8_t Gen1GameReader::addPokemon(Gen1TrainerPokemon& poke, const char* originalTrainerID, const char* nickname) diff --git a/src/gen1/Gen1IconDecoder.cpp b/src/gen1/Gen1IconDecoder.cpp new file mode 100644 index 0000000..3a79886 --- /dev/null +++ b/src/gen1/Gen1IconDecoder.cpp @@ -0,0 +1,193 @@ +#include "gen1/Gen1IconDecoder.h" +#include "RomReader.h" + +#include + +static uint8_t reverseByte(uint8_t incoming) +{ + uint8_t result = 0; + uint8_t maskIncoming = 0x80; + uint8_t flagOutgoing = 1; + while(maskIncoming) + { + if(incoming & maskIncoming) + { + result |= flagOutgoing; + } + maskIncoming >>= 1; + flagOutgoing <<= 1; + } + return result; +} + +Gen1IconDecoder::Gen1IconDecoder(IRomReader& romReader, Gen1GameType gameType) + : buffer_() + , romReader_(romReader) + , gameType_(gameType) +{ + +} + +/* + * 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* Gen1IconDecoder::decode(Gen1PokemonIconType iconType, bool firstFrame) +{ + const uint32_t romOffset = (gameType_ == Gen1GameType::YELLOW) ? 0x7184D : 0x717C0; + const uint32_t ENTRY_SIZE = 6; + const uint8_t MAX_NUM_TILES = 8; + + uint8_t iconSrcBuffer[GEN1_BYTES_PER_TILE * MAX_NUM_TILES]; + uint16_t pointer; + uint16_t vSpritesTileOffset; + uint8_t numTiles; // total number of tiles of the sprite (to indicate byte size) + uint8_t bankIndex; + uint8_t entryIndex; + // this boolean indicates that the sprite only stores the left half, but it's symmetric + bool isSymmetric; + + 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 == Gen1PokemonIconType::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); + + // HACK: so, in the case that only the left half of the icon is stored, every tile (top left + bottom left) + // gets a separate entry in the table. But that's a bit annoying to work with. Since both tiles are stored + // contiguously on the cartridge, let's just manipulate the numTiles field because we're going to render the tiles + // separately anyway because we're aware that we're in this scenario. + if(numTiles == 1 && isSymmetric) + { + numTiles = 2; + } + } + + romReader_.seekToRomPointer(pointer, bankIndex); + romReader_.read(iconSrcBuffer, GEN1_BYTES_PER_TILE * numTiles); + + if(isSymmetric) + { + const uint8_t numRowBytes = GEN1_BYTES_PER_TILE * GEN1_ICON_HEIGHT_IN_TILES; + const uint8_t* const dataEnd = iconSrcBuffer + numRowBytes; + const uint8_t* cur = iconSrcBuffer; + uint8_t* outCur = buffer_ + numRowBytes; + // in this scenario, the tiles are stored in vertical order, but only the left half (2 tiles) + // for the right half, we're supposed to mirror the bytes + memcpy(buffer_, iconSrcBuffer, numRowBytes); + + while(cur < dataEnd) + { + (*outCur) = reverseByte((*cur)); + + ++cur; + ++outCur; + } + } + else + { + // even though we read the entire icon at once, they are stored in the rom/cartridge in horizontal tile order. + // But WE want to return the data in vertical order + // (this keeps rendering with SpriteRenderer simple) + // so we need to reorder the tiles from horizontal to vertical + memcpy(buffer_, iconSrcBuffer, GEN1_BYTES_PER_TILE); + memcpy(buffer_ + GEN1_BYTES_PER_TILE, iconSrcBuffer + (2 * GEN1_BYTES_PER_TILE), GEN1_BYTES_PER_TILE); + memcpy(buffer_ + (2 * GEN1_BYTES_PER_TILE), iconSrcBuffer + GEN1_BYTES_PER_TILE, GEN1_BYTES_PER_TILE); + memcpy(buffer_ + (3 * GEN1_BYTES_PER_TILE), iconSrcBuffer + (3 * GEN1_BYTES_PER_TILE), GEN1_BYTES_PER_TILE); + } + + return buffer_; +} \ No newline at end of file diff --git a/src/gen2/Gen2GameReader.cpp b/src/gen2/Gen2GameReader.cpp index 7a5a854..47f2b5b 100644 --- a/src/gen2/Gen2GameReader.cpp +++ b/src/gen2/Gen2GameReader.cpp @@ -281,7 +281,11 @@ static bool hasBackupSave(ISaveManager& saveManager, bool isCrystal) } Gen2GameReader::Gen2GameReader(IRomReader &romReader, ISaveManager &saveManager, Gen2GameType gameType) - : romReader_(romReader), saveManager_(saveManager), spriteDecoder_(romReader), gameType_(gameType) + : romReader_(romReader) + , saveManager_(saveManager) + , spriteDecoder_(romReader) + , iconDecoder_(romReader, gameType) + , gameType_(gameType) { } @@ -462,38 +466,9 @@ uint8_t *Gen2GameReader::decodeSprite(uint8_t bankIndex, uint16_t pointer) return spriteDecoder_.decode(bankIndex, pointer); } -uint8_t *Gen2GameReader::decodePokemonIcon(Gen2PokemonIconType iconType, SpriteRenderer& renderer, SpriteRenderer::OutputFormat outputFormat, bool firstFrame) +uint8_t *Gen2GameReader::decodePokemonIcon(Gen2PokemonIconType iconType, bool firstFrame) { - const uint32_t romOffset = (gameType_ == Gen2GameType::CRYSTAL) ? 0x8EBBF : 0x8EA70; - const uint8_t MAX_NUM_TILES = 4; - 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 uint8_t TILES_PER_ICON = 4; - const uint32_t ENTRY_SIZE = 2; - uint8_t iconSrcBuffer[MAX_NUM_TILES * BYTES_PER_TILE]; - const uint8_t bankIndex = 0x23; - uint16_t pointer; - - // read IconPointers table entry - romReader_.seek(romOffset); - romReader_.advance(((uint32_t)iconType) * ENTRY_SIZE); - romReader_.readUint16(pointer); - - // now jump to the pointer to find the sprite - romReader_.seekToRomPointer(pointer, bankIndex); - - // every sprite contains 2 frames. - // if we want the second one, we need to skip 1 16x16 sprite - if(!firstFrame) - { - romReader_.advance(TILES_PER_ICON * BYTES_PER_TILE); - } - - romReader_.read(iconSrcBuffer, TILES_PER_ICON * BYTES_PER_TILE); - - return renderer.draw(iconSrcBuffer, outputFormat, gen2_iconColorPalette, 2, 2, SpriteRenderer::TileOrder::HORIZONTAL); + return iconDecoder_.decode(iconType, firstFrame); } uint8_t Gen2GameReader::addPokemon(Gen2TrainerPokemon &poke, bool isEgg, const char *originalTrainerID, const char *nickname) diff --git a/src/gen2/Gen2IconDecoder.cpp b/src/gen2/Gen2IconDecoder.cpp new file mode 100644 index 0000000..77b3b8b --- /dev/null +++ b/src/gen2/Gen2IconDecoder.cpp @@ -0,0 +1,54 @@ +#include "gen2/Gen2IconDecoder.h" +#include "RomReader.h" + +#include + +Gen2IconDecoder::Gen2IconDecoder(IRomReader& romReader, Gen2GameType gameType) + : buffer_() + , romReader_(romReader) + , gameType_(gameType) +{ +} + +uint8_t* Gen2IconDecoder::decode(Gen2PokemonIconType iconType, bool firstFrame) +{ + const uint32_t romOffset = (gameType_ == Gen2GameType::CRYSTAL) ? 0x8EBBF : 0x8EA70; + const uint8_t MAX_NUM_TILES = 4; + 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 uint8_t TILES_PER_ICON = 4; + const uint32_t ENTRY_SIZE = 2; + uint8_t iconSrcBuffer[MAX_NUM_TILES * BYTES_PER_TILE]; + const uint8_t bankIndex = 0x23; + uint16_t pointer; + + // read IconPointers table entry + romReader_.seek(romOffset); + romReader_.advance(((uint32_t)iconType) * ENTRY_SIZE); + romReader_.readUint16(pointer); + + // now jump to the pointer to find the sprite + romReader_.seekToRomPointer(pointer, bankIndex); + + // every sprite contains 2 frames. + // if we want the second one, we need to skip 1 16x16 sprite + if(!firstFrame) + { + romReader_.advance(TILES_PER_ICON * BYTES_PER_TILE); + } + + romReader_.read(iconSrcBuffer, TILES_PER_ICON * BYTES_PER_TILE); + + // even though we read the entire icon at once, they are stored in the rom/cartridge in horizontal tile order. + // But WE want to return the data in vertical order + // (this keeps rendering with SpriteRenderer simple) + // so we need to reorder the tiles from horizontal to vertical + memcpy(buffer_, iconSrcBuffer, GEN2_BYTES_PER_TILE); + memcpy(buffer_ + GEN2_BYTES_PER_TILE, iconSrcBuffer + (2 * GEN2_BYTES_PER_TILE), GEN2_BYTES_PER_TILE); + memcpy(buffer_ + (2 * GEN2_BYTES_PER_TILE), iconSrcBuffer + GEN2_BYTES_PER_TILE, GEN2_BYTES_PER_TILE); + memcpy(buffer_ + (3 * GEN2_BYTES_PER_TILE), iconSrcBuffer + (3 * GEN2_BYTES_PER_TILE), GEN2_BYTES_PER_TILE); + + return buffer_; +} \ No newline at end of file