Initial import

This commit is contained in:
risingPhil 2024-06-11 21:45:59 +02:00 committed by Philippe Symons
parent 2b1041b560
commit 5954a2a036
48 changed files with 15112 additions and 0 deletions

54
CREDITS Normal file
View File

@ -0,0 +1,54 @@
CREDITS
================================================================
First of all, I want to thank the people behind Bulbapedia. The documentation was extensive and without it, I never would've started on this project.
Secondly, I'd like to extend my thanks to Alex "IsoFrieze" Losego from the Retro Game Mechanics Explained Youtube channel. His videos on the topic of Gen 1 sprite
decompression were pretty helpful.
I don't know the identity of the authors of the Pokémon gen 1 and 2 ROM maps on datacrystal.romhacking.net, but these were a valuable resource as well.
I also want to thank the community behind the pokecrystal project. Without them, I would'nt as easily have found the rom offsets of the pokémon front sprites for Pokémon Crystal.
And of course I'd like to extend my thanks to everyone else in the pokémunity who wrote documentation or code for these old pokémon games.
Below, I've listed an overview of the documents and other resources used during the initial development of this library.
While several github repos are listed, I didn't copy any of the code. Instead I used these as reference while debugging some of the bugs I encountered.
Actual links:
=================================================================
https://github.com/magical/pokemon-sprites-rby
https://www.youtube.com/watch?v=aF1Yw_wu2cM
https://glitchcity.wiki/wiki/Sprite_decompression_(Generation_I)
https://rgmechex.com/tech/gen1decompress.html
https://github.com/seanmorris/pokemon-parser/blob/master/source/PokemonRom.js
https://bulbapedia.bulbagarden.net/wiki/Pok%C3%A9mon_species_data_structure_(Generation_I)#:~:text=The%20Pok%C3%A9mon%20species%20data%20structure,order%20rather%20than%20index%20number.
https://www.smogon.com/smog/issue27/glitch
https://datacrystal.romhacking.net/wiki/Pok%C3%A9mon_Red_and_Blue/ROM_map
https://datacrystal.romhacking.net/wiki/Pok%C3%A9mon_Gold_and_Silver/ROM_map
https://bulbapedia.bulbagarden.net/wiki/Stat
https://bulbapedia.bulbagarden.net/wiki/Individual_values#:~:text=In%20Generation%20I%20and%20II,in%20binary%200000%2D1111).
https://bulbapedia.bulbagarden.net/wiki/Effort_values
https://bulbapedia.bulbagarden.net/wiki/List_of_European_language_event_Pok%C3%A9mon_distributions_(Generation_I)
https://bulbapedia.bulbagarden.net/wiki/List_of_European_language_event_Pok%C3%A9mon_distributions_(Generation_II)
https://bulbapedia.bulbagarden.net/wiki/List_of_PCNY_event_Pok%C3%A9mon_distributions_(Generation_II)
https://bulbapedia.bulbagarden.net/wiki/Save_data_structure_(Generation_I)
https://bulbapedia.bulbagarden.net/wiki/Pok%C3%A9mon_data_structure_(Generation_I)
https://bulbapedia.bulbagarden.net/wiki/Save_data_structure_(Generation_II)
https://bulbapedia.bulbagarden.net/wiki/List_of_Pok%C3%A9mon_by_index_number_(Generation_I)
https://bulbapedia.bulbagarden.net/wiki/Experience#Relation_to_level
https://github.com/LinusU/pokemon-sprite-compression
https://gbdev.io/gb-asm-tutorial/part1/tiles.html#:~:text=The%20concept%20of%20a%20%E2%80%9Ctile,referred%20to%20as%20meta%2Dtiles.
https://gbdev.io/pandocs/Palettes.html
https://bulbapedia.bulbagarden.net/wiki/List_of_color_palettes_by_index_number_(Generation_I)
https://github.com/xvillaneau/poke-sprite-python/tree/trunk
https://www.nesdev.org/wiki/Tile_compression#Pok%C3%A9mon_LZ
https://github.com/LinusU/pokemon-sprite-compression/blob/main/doc/gen2.txt
https://github.com/pret/pokecrystal
https://www.huderlem.com/demos/gameboypalette.html
https://bulbapedia.bulbagarden.net/wiki/Shiny_Pok%C3%A9mon#Determining_Shininess
https://bulbapedia.bulbagarden.net/wiki/List_of_moves
https://bulbapedia.bulbagarden.net/wiki/List_of_items_by_index_number_(Generation_II)

58
Makefile Normal file
View File

@ -0,0 +1,58 @@
# # 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 := src
# 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))
PNG_SUPPORT ?= 1
# Add PNG support flag if PNG_SUPPORT is set to 1
ifeq ($(PNG_SUPPORT),1)
CXXFLAGS += -DPNG_SUPPORT
MAKEOVERRIDES := PNG_SUPPORT=1
endif
# Ensure necessary directories exist
# This function ensures the directory for the target exists
define make_directory
@mkdir -p $(dir $@)
endef
# Target executable
TARGET := libpokemegb.a
# Phony targets
.PHONY: all clean
# Default target
all: $(TARGET) examples
examples: $(TARGET)
$(MAKE) -C examples $(MAKECMDGOALS) $(MAKEOVERRIDES)
# Rule to build the target executable
$(TARGET): $(OBJS)
@echo "Creating static library $(TARGET)"
ar rcs $@ $(OBJS)
ranlib $(TARGET)
# Rule to compile source files
$(BUILD_DIR)/%.o: $(SRC_DIR)/%.cpp | $(BUILD_DIR) $(C_FILE)
$(make_directory)
$(CXX) $(CXXFLAGS) -c $< -o $@
# Create the build directory if it doesn't exist
$(BUILD_DIR):
mkdir -p $(BUILD_DIR)
# Clean rule
clean:
make -C examples clean $(MAKEOVERRIDES)
rm -rf $(BUILD_DIR) $(TARGET)

36
README.md Normal file
View File

@ -0,0 +1,36 @@
# Introduction
This project implements functionality to read stats, front sprites, Trainers' party/PC boxes and other various properties either from the ROM or from a save file of Gen 1 and Gen 2 Pokémon games.
I'm happy to accept pull requests if the community wants to do them.
# Features
- Decode front sprites from Gen 1 and Gen 2 Pokémon games
- Read Trainers' party and PC boxes from Gen 1 and 2
- Inject Pokémon from past distribution events into your Gen 1/Gen 2 game save
- Inject other pokémon into your Gen 1/Gen 2 game save
# Limitations
- Right now, this library only supports the international versions of the games.
# Dependencies
This library only depends on libc. I specifically wanted to avoid the bloat of libstdc++ or any other library. This choice is to keep this library small and portable.
The library was developed on Linux. I haven't tried building it on Windows or WSL yet, so let me know how that goes :)
# Build
To build it, you can do this:
\#Build with 12 threads (Change the thread number to what you want):
make -j 12
# Goal
This project was built as a stepping stone for the next project I want to do: To experiment with the N64 transfer pak with libdragon. In that project, I want to be able to inject Distribution pokémon into a cartridge using the N64 transfer pak. That's why the initial feature set is what it is.
I also hope that if someone else wants to work on something similar, that he/she finds the functionality they want or inspiration to implement it themselves. I tried to document the resources I used during development in the code.
# Future potential improvements
- Add support for gen 3
- Possibly other additions if the community wants them?

141
docs/gen2.txt Normal file
View File

@ -0,0 +1,141 @@
Copied from https://github.com/LinusU/pokemon-sprite-compression/blob/main/doc/gen2.txt :
POKEMON GRAPHICS COMPRESSION DOCUMENT
Version 1.0
-Pokemon uses a unique compression routine to pack all of those 251
pokemon into a single game boy cartridge. Here's my documentation of this
compression:
Look at the first (high) 3 bits of the first byte -
(Bitwise AND with 224 - 0xE0, binary 11100000).
The value in these high 3 bits are a 'control code' for the processing of
the following information.
There are 7 possible control codes:
In binary, 000, 001, 010, 011, 100, 101, 111.
Keeping the extra 5 bits as zeroes, this gives us the seven hex values of
00, 20, 40, 60, 80, A0, C0 and E0 as control codes.
Look at the last(low) 5 bits of the first byte -
(Bitwise AND with 31 - 0X1F, binary 00011111), call that value x.
00: Read (x + 1) following amount of bytes from the ROM and print to the
graphics output.
20: Read one byte from the ROM, call that value y. Print value (y),
(x+1) number of times to the graphics output.
40: Read two bytes from the ROM, call those values (y-z).
Print y and z alternately (x+1) number of times total to the graphics
output. Example: If x = 5, print yzyzy.
60: Print (x+1) bytes with value zero to the graphics output.
***The control bytes 80, A0 and C0 deal with values already stored in the
graphics output, as a way of 'reusing' data to save space.***
80: Read a byte from the rom, call that value A.
If the high bit of A (Bitwise AND with 128 - 0x80, binary 10000000) is clear:
Read the next byte from the rom, call that value N.
Copy (x+1) consecutive bytes from the graphics buffer to the graphics output,
starting at (A*0x100) + (N+1).
If the high bit of A is set:
(AND A with 127(0x7F) to clear the high bit)
Copy (x+1) consecutive bytes from the graphics buffer to the graphics output,
starting A bytes back from the end of the buffer.
A0: Read a byte from the rom, call that value A.
If the high bit of A is clear:
Read the next byte from the rom, call that value N.
Copy (x+1) consecutive bytes from the graphics buffer to the graphics output,
starting at (A*0x100) + (N+1), REVERSING the bit order.
Example: Byte k in graphics buffer = 0xD7 (11010111 binary).
New byte to be printed to graphics output = 0xEB(11101011 binary).
If the high bit of A is set:
(clear the high bit of A)
Copy (x+1) consecutive bytes from the graphics buffer to the graphics output,
starting A bytes back from the end of the buffer, REVERSING the bit order.
C0: Read a byte from the rom, call that value A.
If the high bit of A is clear:
Read the next byte from the rom, call that value N.
Copy (x+1) reverse consecutive bytes from the graphics buffer to the graphics
output, starting at (A*0x100) + (N+1).
"reverse consecutive" meaning starting from the above point in the graphics
buffer, read a byte from buffer and print to the end of the output,
then read the previous byte and print to the end of the output, etc.
If the high bit of A is set:
(clear the high bit of A)
Copy (x+1) reverse consecutive bytes from the graphics buffer to the graphics
output, starting A bytes back from the end of the buffer.
E0: The E0 control code is special:
Take the high 3 bits of x (remember, x is currently the low 5 bits of the
first byte, so AND that byte with (28decimal/0x1C/00011100 binary),
this will be the new 3-bit control code.
Get another byte from the ROM, call it w.
The new X value consists of (the lowest two bits of the old x
(AND with 03, 00000011 binary) * 0x100) + w.
Go back to the top of the loop with the new control code and x value.
That's the compression!
Here are some addresses that might be handy:
(NOTE: UNOWN, pokemon # 201 has its graphics stored in some special
manner (most likely because there are 26 separate images),
thus its pointer points to nothing. I've simply skipped UNOWN #201
in my program, but if someone else wants to look for him, go ahead and let
me know and i'll update this doc.)
0x48000 - start of Pokemon graphics pointers - 6 bytes per pokemon.
First three bytes is pointer to the pokemon facing you,
next three is pointer to the pokemon with its back facing you.
Here is the layout of the 3 pointer bytes:
1st byte: Bank byte. Byte * 4000h to get the bank address. However:
If 1st byte = 0x13 then change it to 0x1F
If 1st byte = 0x14 then change it to 0x20
If 1st byte = 0x1F then change it to 0x2E
Don't ask why; it's simply in the game's code.
2nd byte: Low byte of address.
3rd byte: High byte of address.
To correctly calculate the ROM address of the start of the graphics data
using the 3-byte pointer:
Address = (byte 1) * 0x4000 + (byte 3) * 0x100 + (byte 2)
0xAD3D - Start of Palette data. 8 bytes for each pokemon.
2 color schemes for each pokemon, first 4 bytes = normal color scheme,
next 4 bytes = "shiny" pokemon color scheme.
Each pokemon has four colors: white(color 0), black(color 3), and two colors
defined by the 4 palette bytes(1 and 2), 2 bytes per color.
The bytes use RGB. First, rearrange the two bytes so the second byte is
first (common 16-bit practice). The RGB arrangement is as follows for the
16-bit pair:
0bbbbbgggggrrrrr, each color attribute having five bytes,
and the high bit unused.
That's it!
My QBasic program uses all of the information above to decompress
Pokemon images from the ROM and output them to the screen.
Check out the source for further clarification QBasic-style.
-necrosaro (radimvice@geocities.com)
http://members.aol.com/sabindude/pokemon.html
-For an archive of all 251 pokemon from Gold and Silver, shiny and regular,
facing fowards and backwards, in PNG format, go to
http://www.geocities.com/radimvice/pokepng.zip

18
examples/Makefile Normal file
View File

@ -0,0 +1,18 @@
# Filter out the unwanted MAKEFLAGS
.PHONY: all clean
all:
$(MAKE) -C do_stuff_gen1 $(MAKECMDGOALS) $(MAKEOVERRIDES)
$(MAKE) -C do_stuff_gen2 $(MAKECMDGOALS) $(MAKEOVERRIDES)
$(MAKE) -C encodeText_gen1 $(MAKECMDGOALS) $(MAKEOVERRIDES)
$(MAKE) -C encodeText_gen2 $(MAKECMDGOALS) $(MAKEOVERRIDES)
$(MAKE) -C decodeSprite $(MAKECMDGOALS) $(MAKEOVERRIDES)
$(MAKE) -C addDistributionPoke $(MAKECMDGOALS) $(MAKEOVERRIDES)
clean:
$(MAKE) -C do_stuff_gen1 clean
$(MAKE) -C do_stuff_gen2 clean
$(MAKE) -C encodeText_gen1 clean
$(MAKE) -C encodeText_gen2 clean
$(MAKE) -C decodeSprite clean
$(MAKE) -C addDistributionPoke 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 := ../../addDistributionPoke
# 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,171 @@
#include "gen1/Gen1GameReader.h"
#include "gen1/Gen1DistributionPokemon.h"
#include "gen2/Gen2GameReader.h"
#include "gen2/Gen2DistributionPokemon.h"
#include "RomReader.h"
#include "SaveManager.h"
#include "SpriteRenderer.h"
#include "utils.h"
#include <cstdio>
#include <cstring>
#include <cstdlib>
static void showAvailableGen1DistributionPokeMenu(Gen1GameReader& gameReader)
{
const Gen1DistributionPokemon** mainList;
uint32_t listSize;
char input[100];
uint8_t userChoice;
gen1_getMainDistributionPokemonList(mainList, listSize);
printf("Available distribution pokémon:\n");
for(uint8_t i = 0; i < listSize; ++i)
{
printf("\t%hhu.) %s\n", i, mainList[i]->name);
}
printf("Choose: (0-%hhu): ", listSize - 1);
if (fgets(input, sizeof(input), stdin) == NULL)
{
fprintf(stderr, "Error reading input\n");
return;
}
// Remove the newline character if it exists
input[strcspn(input, "\n")] = '\0';
userChoice = (uint8_t)strtoul(input, 0, 10);
if(userChoice > listSize - 1)
{
fprintf(stderr, "Invalid choice, try again!");
return;
}
printf("Congratulations! You got a %s!\n", gameReader.getPokemonName(mainList[userChoice]->poke.poke_index));
gameReader.addDistributionPokemon((*mainList[userChoice]));
}
static void showAvailableGen2DistributionPokeMenu(Gen2GameReader& gameReader)
{
const Gen2DistributionPokemon** mainList;
const Gen2DistributionPokemon** pcnyList;
const Gen2DistributionPokemon** listToUse;
uint32_t mainListSize;
uint32_t pcnyListSize;
char input[100];
uint8_t userChoice;
uint8_t i;
gen2_getMainDistributionPokemonList(mainList, mainListSize);
gen2_getPokemonCenterNewYorkDistributionPokemonList(pcnyList, pcnyListSize);
printf("Available distribution pokémon:\n");
for(i = 0; i < mainListSize; ++i)
{
printf("\t%hhu.) %s\n", i, mainList[i]->name);
}
for(i = 0; i < pcnyListSize; ++i)
{
printf("\t%hhu.) PCNY %s\n", i + mainListSize, pcnyList[i]->name);
}
printf("Choose: (0-%hhu): ", mainListSize + pcnyListSize - 1);
if (fgets(input, sizeof(input), stdin) == NULL)
{
fprintf(stderr, "Error reading input\n");
return;
}
// Remove the newline character if it exists
input[strcspn(input, "\n")] = '\0';
userChoice = (uint8_t)strtoul(input, 0, 10);
if(userChoice >= mainListSize + pcnyListSize)
{
fprintf(stderr, "Invalid choice, try again!");
return;
}
if(userChoice >= mainListSize)
{
listToUse = pcnyList;
userChoice -= mainListSize;
}
else
{
listToUse = mainList;
}
if(listToUse[userChoice]->isEgg)
{
printf("Congratulations! You got an %s EGG! Take good care of it!\n", gameReader.getPokemonName(listToUse[userChoice]->poke.poke_index));
}
else
{
printf("Congratulations! You got a %s!\n", gameReader.getPokemonName(listToUse[userChoice]->poke.poke_index));
}
gameReader.addDistributionPokemon((*listToUse[userChoice]));
gameReader.finishSave();
}
int main(int argc, char** argv)
{
if(argc != 3)
{
fprintf(stderr, "Usage: addDistributionPoke <path/to/rom.gbc> <path/to/file.sav>\n");
return 1;
}
uint8_t* romBuffer;
uint8_t* saveBuffer;
uint32_t romFileSize;
uint32_t saveFileSize;
romBuffer = readFileIntoBuffer(argv[1], romFileSize);
if(!romBuffer)
{
fprintf(stderr, "ERROR: Couldn't read file %s\n", argv[1]);
return 1;
}
saveBuffer = readFileIntoBuffer(argv[2], saveFileSize);
if(!saveBuffer)
{
fprintf(stderr, "ERROR: Couldn't read file %s\n", argv[2]);
return 1;
}
GameboyCartridgeHeader cartridgeHeader;
BufferBasedRomReader romReader(romBuffer, romFileSize);
BufferBasedSaveManager saveManager(saveBuffer, saveFileSize);
readGameboyCartridgeHeader(romReader, cartridgeHeader);
//check if we're dealing with gen 1
const Gen1GameType gen1Type = gen1_determineGameType(cartridgeHeader);
const Gen2GameType gen2Type = gen2_determineGameType(cartridgeHeader);
if(gen1Type != Gen1GameType::INVALID)
{
Gen1GameReader gameReader(romReader, saveManager, gen1Type);
showAvailableGen1DistributionPokeMenu(gameReader);
}
else if(gen2Type != Gen2GameType::INVALID)
{
Gen2GameReader gameReader(romReader, saveManager, gen2Type);
showAvailableGen2DistributionPokeMenu(gameReader);
}
FILE* f = fopen(argv[2], "w");
fwrite(saveBuffer, 1, saveFileSize, f);
fclose(f);
free(romBuffer);
free(saveBuffer);
romBuffer = 0;
saveBuffer = 0;
return 0;
}

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 := ../../decodeSprite
# 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,136 @@
#include "gen1/Gen1GameReader.h"
#include "gen2/Gen2GameReader.h"
#include "RomReader.h"
#include "SaveManager.h"
#include "SpriteRenderer.h"
#include "utils.h"
#include <cstdio>
#include <cstring>
#include <cstdlib>
static void decodeGen1Sprite(Gen1GameReader& gameReader, uint8_t pokeIndex)
{
SpriteRenderer renderer;
Gen1PokeStats stats;
uint8_t* spriteBuffer;
uint8_t* rgbBuffer;
char fileNameBuf[50];
uint16_t colorPalette[4];
const char* pokeName;
if(!gameReader.isValidIndex(pokeIndex))
{
fprintf(stderr, "ERROR: index %hhu is not valid!\n", pokeIndex);
return;
}
pokeName = gameReader.getPokemonName(pokeIndex);
printf("Decoding %s\n", pokeName);
gameReader.readPokemonStatsForIndex(pokeIndex, stats);
gameReader.readColorPalette(gameReader.getColorPaletteIndexByPokemonNumber(stats.pokedex_number), colorPalette);
spriteBuffer = gameReader.decodeSprite(stats.sprite_bank, stats.pointer_to_frontsprite);
// we'll now write the decoded sprite in binary form. This is in gameboy format essentially.
snprintf(fileNameBuf, sizeof(fileNameBuf), "%s.bin", pokeName);
FILE* f = fopen(fileNameBuf, "w");
fwrite(spriteBuffer, 1, GEN1_DECODED_SPRITE_BUFFER_SIZE_IN_BYTES, f);
fclose(f);
// convert it to RGB
rgbBuffer = renderer.drawRGB(spriteBuffer, colorPalette, 7, 7);
// now convert the RGB data and output it as PNG
snprintf(fileNameBuf, sizeof(fileNameBuf), "%s.png", pokeName);
write_png(fileNameBuf, rgbBuffer, 56, 56);
}
static void decodeGen2Sprite(Gen2GameReader& gameReader, uint8_t pokeIndex)
{
SpriteRenderer renderer;
Gen2PokeStats stats;
uint8_t* spriteBuffer;
uint8_t* rgbBuffer;
char fileNameBuf[50];
uint16_t colorPalette[4];
const char* pokeName;
uint8_t spriteWidthInTiles;
uint8_t spriteHeightInTiles;
uint8_t bankIndex;
uint16_t pointer;
if(!gameReader.isValidIndex(pokeIndex))
{
fprintf(stderr, "ERROR: index %hhu is not valid!\n", pokeIndex);
return;
}
pokeName = gameReader.getPokemonName(pokeIndex);
printf("Decoding %s\n", pokeName);
gameReader.readPokemonStatsForIndex(pokeIndex, stats);
gameReader.readSpriteDimensions(stats, spriteWidthInTiles, spriteHeightInTiles);
gameReader.readColorPaletteForPokemon(pokeIndex, false, colorPalette);
gameReader.readFrontSpritePointer(pokeIndex, bankIndex, pointer);
spriteBuffer = gameReader.decodeSprite(bankIndex, pointer);
// we'll now write the decoded sprite in binary form. This is in gameboy format essentially.
snprintf(fileNameBuf, sizeof(fileNameBuf), "%s.bin", pokeName);
FILE* f = fopen(fileNameBuf, "w");
fwrite(spriteBuffer, 1, GEN2_SPRITE_BUFFER_SIZE_IN_BYTES, f);
fclose(f);
// convert it to RGB
rgbBuffer = renderer.drawRGB(spriteBuffer, colorPalette, spriteWidthInTiles, spriteHeightInTiles);
// now convert the RGB data and output it as PNG
snprintf(fileNameBuf, sizeof(fileNameBuf), "%s.png", pokeName);
write_png(fileNameBuf, rgbBuffer, spriteWidthInTiles * 8, spriteHeightInTiles * 8);
}
int main(int argc, char** argv)
{
if(argc != 3)
{
fprintf(stderr, "Usage: decodeSprite <path/to/rom.gbc> <poke_index>\n");
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(0, 0);
readGameboyCartridgeHeader(romReader, cartridgeHeader);
//check if we're dealing with gen 1
const Gen1GameType gen1Type = gen1_determineGameType(cartridgeHeader);
const Gen2GameType gen2Type = gen2_determineGameType(cartridgeHeader);
if(gen1Type != Gen1GameType::INVALID)
{
Gen1GameReader gameReader(romReader, saveManager, gen1Type);
decodeGen1Sprite(gameReader, strtoul(argv[2], 0, 0));
}
else if(gen2Type != Gen2GameType::INVALID)
{
Gen2GameReader gameReader(romReader, saveManager, gen2Type);
decodeGen2Sprite(gameReader, strtoul(argv[2], 0, 0));
}
free(romBuffer);
romBuffer = 0;
return 0;
}

View File

@ -0,0 +1,4 @@
[Dolphin]
PreviewsShown=true
Timestamp=2024,6,11,14,31,58
Version=4

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 := ../../do_stuff_gen1
# 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,258 @@
#include "RomReader.h"
#include "SaveManager.h"
#include "gen1/Gen1GameReader.h"
#include "SpriteRenderer.h"
#include "utils.h"
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <time.h>
// This example application just does some random stuff with Gen 1 game + save.
// This was merely used during development of this library and is therefore unfocused/chaotic in nature.
#define PRINT_POKESTATS 1
#define DECODE_SPRITES 1
#ifdef PRINT_POKESTATS
static void printPokestats(const char* prefix, Gen1PokeStats& stats)
{
printf("%snumber: %hhu\n", prefix, stats.pokedex_number);
printf("%sBase HP: %hhu\n", prefix, stats.base_hp);
printf("%sBase Atk: %hhu\n", prefix, stats.base_attack);
printf("%sBase Def: %hhu\n", prefix, stats.base_defense);
printf("%sBase Spd: %hhu\n", prefix, stats.base_speed);
printf("%sBase Spec: %hhu\n", prefix, stats.base_special);
printf("%sType1: 0x%hhx\n", prefix, stats.type1);
printf("%sType2: 0x%hhx\n", prefix, stats.type2);
printf("%sCatch rate: %hhu\n", prefix, stats.catch_rate);
printf("%sBase Exp Yield: %hhu\n", prefix, stats.base_exp_yield);
printf("%sFront Sprite Dimensions: 0x%hhx\n", prefix, stats.front_sprite_dimensions);
printf("%sSprite bank: %hhx\n", prefix, stats.sprite_bank);
printf("%sFront Sprite Ptr: 0x%hx\n", prefix, stats.pointer_to_frontsprite);
printf("%sBack Sprite Ptr: %hx\n", prefix, stats.pointer_to_backsprite);
printf("%sBase Attacks: 0x%hhx 0x%hhx 0x%hhx 0x%hhx\n", prefix, stats.lvl1_attacks[0], stats.lvl1_attacks[1], stats.lvl1_attacks[2], stats.lvl1_attacks[3]);
printf("%sGrowth Rate: %hhu\n", prefix, stats.growth_rate);
printf("%sTM/HM Flags: 0x%hhx 0x%hhx 0x%hhx 0x%hhx 0x%hhx 0x%hhx 0x%hhx\n", prefix, stats.tm_hm_flags[0], stats.tm_hm_flags[1], stats.tm_hm_flags[2], stats.tm_hm_flags[3], stats.tm_hm_flags[4], stats.tm_hm_flags[5], stats.tm_hm_flags[6]);
}
#endif
static void printTrainerPokemon(const char* prefix, const Gen1TrainerPokemon& poke, Gen1GameReader& reader)
{
printf("%s\tNumber: %hhu\n", prefix, reader.getPokemonNumber(poke.poke_index));
printf("%s\tIndex: %hhu\n", prefix, poke.poke_index);
printf("%s\tCurrent HP: %hu\n", prefix, poke.current_hp);
printf("%s\tLevel: %hhu\n", prefix, poke.level);
printf("%s\tStatus Condition: 0x%hhx\n", prefix, poke.status_condition);
printf("%s\tType 1: 0x%hhx\n", prefix, poke.type1);
printf("%s\tType 2: 0x%hhx\n", prefix, poke.type2);
printf("%s\tCatch Rate/Held Item: 0x%hhx\n", prefix, poke.catch_rate_or_held_item);
printf("%s\tMove 1: 0x%hhx\n", prefix, poke.index_move1);
printf("%s\tMove 2: 0x%hhx\n", prefix, poke.index_move2);
printf("%s\tMove 3: 0x%hhx\n", prefix, poke.index_move3);
printf("%s\tMove 4: 0x%hhx\n", prefix, poke.index_move4);
printf("%s\tOriginal Trainer ID: 0x%hx\n", prefix, poke.original_trainer_ID);
printf("%s\tExp: %u\n", prefix, poke.exp);
printf("%s\tHP EV: %hu\n", prefix, poke.hp_effort_value);
printf("%s\tAtk EV: %hu\n", prefix, poke.atk_effort_value);
printf("%s\tDef EV: %hu\n", prefix, poke.def_effort_value);
printf("%s\tSpeed EV: %hu\n", prefix, poke.speed_effort_value);
printf("%s\tSpecial EV: %hu\n", prefix, poke.special_effort_value);
printf("%s\tIV: 0x%hhx 0x%hhx\n", prefix, poke.iv_data[0], poke.iv_data[1]);
printf("%s\tPP Move 1: %hhu\n", prefix, poke.pp_move1);
printf("%s\tPP Move 2: %hhu\n", prefix, poke.pp_move2);
printf("%s\tPP Move 3: %hhu\n", prefix, poke.pp_move3);
printf("%s\tPP Move 4: %hhu\n", prefix, poke.pp_move4);
printf("%s\tMax HP: %hu\n", prefix, poke.max_hp);
printf("%s\tAtk: %hu\n", prefix, poke.atk);
printf("%s\tDef: %hu\n", prefix, poke.def);
printf("%s\tSpeed: %hu\n", prefix, poke.speed);
printf("%s\tSpecial: %hu\n", prefix, poke.special);
// test our implementation of calculatePokeStat()
Gen1PokeStats stats;
reader.readPokemonStatsForIndex(poke.poke_index, stats);
printf("%s\tMax HP calculated: %hu\n", prefix, calculatePokeStat(PokeStat::HP, stats.base_hp, getStatIV(PokeStat::HP, poke.iv_data), poke.hp_effort_value, poke.level));
printf("%s\tAtk calculated: %hu\n", prefix, calculatePokeStat(PokeStat::ATK, stats.base_attack, getStatIV(PokeStat::ATK, poke.iv_data), poke.atk_effort_value, poke.level));
printf("%s\tDef calculated: %hu\n", prefix, calculatePokeStat(PokeStat::DEF, stats.base_defense, getStatIV(PokeStat::DEF, poke.iv_data), poke.def_effort_value, poke.level));
printf("%s\tSpeed calculated: %hu\n", prefix, calculatePokeStat(PokeStat::SPEED, stats.base_speed, getStatIV(PokeStat::SPEED, poke.iv_data), poke.speed_effort_value, poke.level));
printf("%s\tSpecial calculated: %hu\n", prefix, calculatePokeStat(PokeStat::SPECIAL, stats.base_special, getStatIV(PokeStat::SPECIAL, poke.iv_data), poke.special_effort_value, poke.level));
}
static void printParty(const char* prefix, Gen1GameReader& reader)
{
Gen1Party party = reader.getParty();
Gen1TrainerPokemon poke;
const uint8_t numPokemon = party.getNumberOfPokemon();
printf("%sNumber: %hhu\n", prefix, numPokemon);
for(uint8_t i=0; i < numPokemon; ++i)
{
party.getPokemon(i, poke);
printf("%s%s (%s)\n", prefix, party.getPokemonNickname(i), reader.getPokemonName(poke.poke_index));
printTrainerPokemon(prefix, poke, reader);
}
}
static void printBox(const char* prefix, Gen1GameReader& reader, uint8_t boxIndex)
{
Gen1Box box = reader.getBox(boxIndex);
Gen1TrainerPokemon poke;
const uint8_t numPokemon = box.getNumberOfPokemon();
printf("%sBox %hhu:\n", prefix, boxIndex + 1);
printf("%sNumber: %hhu\n", prefix, numPokemon);
for(uint8_t i=0; i < numPokemon; ++i)
{
box.getPokemon(i, poke);
printf("%s%s (%s)\n", prefix, box.getPokemonNickname(i), reader.getPokemonName(poke.poke_index));
printTrainerPokemon(prefix, poke, reader);
}
}
static void print_usage()
{
printf("Usage: do_stuff_gen1 <path/to/rom.gbc> <path/to/file.sav>\n");
}
int main(int argc, char** argv)
{
srand(time(NULL));
if(argc != 3)
{
print_usage();
return 1;
}
uint8_t* romBuffer;
uint8_t* savBuffer;
uint32_t romFileSize;
uint32_t savFileSize;
romBuffer = readFileIntoBuffer(argv[1], romFileSize);
if(!romBuffer)
{
fprintf(stderr, "ERROR: Couldn't read file %s\n", argv[1]);
return 1;
}
savBuffer = readFileIntoBuffer(argv[2], savFileSize);
if(!savBuffer)
{
fprintf(stderr, "ERROR: Couldn't read file %s\n", argv[2]);
free(romBuffer);
romBuffer = nullptr;
return 1;
}
GameboyCartridgeHeader cartridgeHeader;
BufferBasedRomReader romReader(romBuffer, romFileSize);
BufferBasedSaveManager saveManager(savBuffer, savFileSize);
readGameboyCartridgeHeader(romReader, cartridgeHeader);
printGameboyCartridgeHeader(cartridgeHeader);
const Gen1GameType gameType = gen1_determineGameType(cartridgeHeader);
if(gameType == Gen1GameType::INVALID)
{
fprintf(stderr, "ERROR: Not a valid Gen 1 Pokémon game!\n");
return 1;
}
Gen1GameReader gen1Reader(romReader, saveManager, gameType);
Gen1PokeStats stats;
#ifdef PRINT_POKESTATS
printf("Pokémon names:\n");
// Note: indices are 1-based: https://bulbapedia.bulbagarden.net/wiki/List_of_Pok%C3%A9mon_by_index_number_(Generation_I)
for(uint8_t i = 1; i < 191; ++i)
{
gen1Reader.readPokemonStatsForIndex(i, stats);
printf("\t%hhu: %s\n", i, gen1Reader.getPokemonName(i));
printPokestats("\t\t", stats);
}
printf("end\n");
#endif
printf("Save game:\n");
printf("\tTrainer: %s (0x%hx)\n", gen1Reader.getTrainerName(), gen1Reader.getTrainerID());
printf("\tRival: %s\n", gen1Reader.getRivalName());
printf("\tSeen: %hhu\n", gen1Reader.getPokedexCounter(POKEDEX_SEEN));
printf("\tOwned: %hhu\n", gen1Reader.getPokedexCounter(POKEDEX_OWNED));
printf("\tParty:\n");
printParty("\t\t", gen1Reader);
printBox("\t\t", gen1Reader, 0);
printBox("\t\t", gen1Reader, 1);
#if 0
printf("Main checksum valid: %d\n", gen1Reader.isMainChecksumValid());
printf("Bank 2 valid: %d\n", gen1Reader.isWholeBoxBankValid(2));
printf("Bank 3 valid: %d\n", gen1Reader.isWholeBoxBankValid(3));
uint8_t currentBoxIndex = gen1Reader.getCurrentBoxIndex();
for(uint8_t i = 0; i < 13; ++i)
{
Gen1Box box = gen1Reader.getBox(i);
printf("Box %hhu valid: %d, currentBoxIndex=%hhu\n", (i + 1), box.isChecksumValid(currentBoxIndex), currentBoxIndex);
}
#endif
#if DECODE_SPRITES
SpriteRenderer renderer;
uint8_t* spriteBuffer;
uint8_t* rgbBuffer;
char fileNameBuf[50];
uint16_t colorPalette[4];
const char* pokeName;
for(uint8_t i = 1; i < 191; ++i)
{
if(!gen1Reader.isValidIndex(i))
{
continue;
}
pokeName = gen1Reader.getPokemonName(i);
printf("Decoding %s\n", pokeName);
gen1Reader.readPokemonStatsForIndex(i, stats);
gen1Reader.readColorPalette(gen1Reader.getColorPaletteIndexByPokemonNumber(stats.pokedex_number), colorPalette);
spriteBuffer = gen1Reader.decodeSprite(stats.sprite_bank, stats.pointer_to_frontsprite);
rgbBuffer = renderer.drawRGB(spriteBuffer, colorPalette, 7, 7);
snprintf(fileNameBuf, sizeof(fileNameBuf), "%s.png", pokeName);
write_png(fileNameBuf, rgbBuffer, 56, 56);
}
#endif
#if 0
Gen1TrainerPokemon bulby;
Gen1Party party = gen1Reader.getParty();
party.getPokemon(0, bulby);
gen1Reader.addPokemon(bulby, nullptr, "bulma");
gen1Reader.addPokemon(bulby, nullptr, "bumma");
gen1Reader.addPokemon(bulby, nullptr, "bulva");
gen1Reader.addPokemon(bulby, nullptr, "bulla");
gen1Reader.addPokemon(bulby, nullptr, "bulbaba");
gen1Reader.addPokemon(bulby, nullptr, "bulpapa");
gen1Reader.addPokemon(bulby, nullptr, "bulmama");
gen1Reader.addPokemon(bulby);
#endif
gen1Reader.addDistributionPokemon(g1_coraChatelineauMew);
gen1Reader.addDistributionPokemon(g1_nintendoPowerPikachu);
FILE* f = fopen("out.sav", "w");
fwrite(savBuffer, 1, savFileSize, f);
fclose(f);
free(romBuffer);
romBuffer = 0;
free(savBuffer);
savBuffer = 0;
return 0;
}

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 := ../../do_stuff_gen2
# 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,272 @@
#include "gen2/Gen2GameReader.h"
#include "SpriteRenderer.h"
#include "RomReader.h"
#include "SaveManager.h"
#include "utils.h"
#include <cstring>
#include <cstdio>
#include <cstdlib>
#include <time.h>
// This example application just does some random stuff with Gen 2 game + save.
// This was merely used during development of this library and is therefore unfocused/chaotic in nature.
//#define DECODE_ALL_FRONT_SPRITES 1
static void printTrainerPokemon(const char* prefix, const Gen2TrainerPokemon& poke, Gen2GameReader& gameReader)
{
Gen2PokeStats stats;
gameReader.readPokemonStatsForIndex(poke.poke_index, stats);
printf("%s\tNumber: %hhu\n", prefix, poke.poke_index);
printf("%s\tType 1: 0x%hhx\n", prefix, stats.type1);
printf("%s\tType 2: 0x%hhx\n", prefix, stats.type2);
printf("%s\tHeld item: 0x%hhx\n", prefix, poke.held_item_index);
printf("%s\tMove 1: 0x%hhx\n", prefix, poke.index_move1);
printf("%s\tMove 2: 0x%hhx\n", prefix, poke.index_move2);
printf("%s\tMove 3: 0x%hhx\n", prefix, poke.index_move3);
printf("%s\tMove 4: 0x%hhx\n", prefix, poke.index_move4);
printf("%s\tOriginal Trainer ID: 0x%hx\n", prefix, poke.original_trainer_ID);
printf("%s\tExp: %u\n", prefix, poke.exp);
printf("%s\tHP EV: %hu\n", prefix, poke.hp_effort_value);
printf("%s\tAtk EV: %hu\n", prefix, poke.atk_effort_value);
printf("%s\tDef EV: %hu\n", prefix, poke.def_effort_value);
printf("%s\tSpeed EV: %hu\n", prefix, poke.speed_effort_value);
printf("%s\tSpecial EV: %hu\n", prefix, poke.special_effort_value);
printf("%s\tIV: 0x%hhx 0x%hhx\n", prefix, poke.iv_data[0], poke.iv_data[1]);
printf("%s\tPP Move 1: %hhu\n", prefix, poke.pp_move1);
printf("%s\tPP Move 2: %hhu\n", prefix, poke.pp_move2);
printf("%s\tPP Move 3: %hhu\n", prefix, poke.pp_move3);
printf("%s\tPP Move 4: %hhu\n", prefix, poke.pp_move4);
printf("%s\tFriendship/Egg cycles: %hhu\n", prefix, poke.friendship_or_remaining_egg_cycles);
printf("%s\tPokerus: 0x%hhx\n", prefix, poke.pokerus);
printf("%s\tCaught data: 0x%hx\n", prefix, poke.caught_data);
printf("%s\tLevel: %hhu\n", prefix, poke.level);
printf("%s\tStatus Condition: 0x%hhx\n", prefix, poke.status_condition);
printf("%s\tCurrent HP: %hu\n", prefix, poke.current_hp);
printf("%s\tMax HP: %hu\n", prefix, poke.max_hp);
printf("%s\tAtk: %hu\n", prefix, poke.atk);
printf("%s\tDef: %hu\n", prefix, poke.def);
printf("%s\tSpeed: %hu\n", prefix, poke.speed);
printf("%s\tSpecial Atk: %hu\n", prefix, poke.special_atk);
printf("%s\tSpecial Def: %hu\n", prefix, poke.special_def);
}
static void printParty(const char* prefix, Gen2GameReader& reader)
{
Gen2Party party = reader.getParty();
Gen2TrainerPokemon poke;
const uint8_t numPokemon = party.getNumberOfPokemon();
printf("%sNumber: %hhu\n", prefix, numPokemon);
for(uint8_t i=0; i < numPokemon; ++i)
{
party.getPokemon(i, poke);
printf("%s%s (%s)\n", prefix, party.getPokemonNickname(i), reader.getPokemonName(poke.poke_index));
printTrainerPokemon(prefix, poke, reader);
}
}
static void printBox(const char* prefix, Gen2GameReader& reader, uint8_t boxIndex)
{
Gen2Box box = reader.getBox(boxIndex);
Gen2TrainerPokemon poke;
const uint8_t numPokemon = box.getNumberOfPokemon();
printf("%sBox %hhu:\n", prefix, boxIndex + 1);
printf("%sNumber: %hhu\n", prefix, numPokemon);
for(uint8_t i=0; i < numPokemon; ++i)
{
box.getPokemon(i, poke);
printf("%s%s (%s)\n", prefix, box.getPokemonNickname(i), reader.getPokemonName(poke.poke_index));
printTrainerPokemon(prefix, poke, reader);
}
}
#ifdef DECODE_ALL_FRONT_SPRITES
static void printStats(const char* prefix, const Gen2PokeStats& stats)
{
printf("%spokedex number:%hhu\n", prefix, stats.pokedex_number);
printf("%sbase hp: %hhu\n", prefix, stats.base_hp);
printf("%sbase atk: %hhu\n", prefix, stats.base_attack);
printf("%sbase def: %hhu\n", prefix, stats.base_defense);
printf("%sbase speed: %hhu\n", prefix, stats.base_speed);
printf("%sbase Sp. atk: %hhu\n", prefix, stats.base_special_attack);
printf("%sbase Sp. def: %hhu\n", prefix, stats.base_special_defense);
printf("%sType 1: 0x%hhx\n", prefix, stats.type1);
printf("%sType 2: 0x%hhx\n", prefix, stats.type2);
printf("%sCatch rate: 0x%hhx\n", prefix, stats.catch_rate);
printf("%sbase exp yield: 0x%hhx\n", prefix, stats.base_exp_yield);
printf("%swild held item 1: 0x%hhx\n", prefix, stats.wild_held_item1);
printf("%swild held item 2: 0x%hhx\n", prefix, stats.wild_held_item2);
printf("%sgender ratio: 0x%hhx\n", prefix, stats.gender_ratio);
printf("%sunknown 1: 0x%hhx\n", prefix, stats.unknown);
printf("%segg cycles: %hhu\n", prefix, stats.egg_cycles);
printf("%sunknown 2: 0x%hhx\n", prefix, stats.unknown2);
printf("%sfront sprite dimensions: 0x%hhx\n", prefix, stats.front_sprite_dimensions);
printf("%sblank: 0x%hhx 0x%hhx 0x%hhx 0x%hhx\n", prefix, stats.blank[0], stats.blank[1], stats.blank[2], stats.blank[3]);
printf("%sgrowth rate: 0x%hhx\n", prefix, stats.growth_rate);
printf("%segg groups: 0x%hhx\n", prefix, stats.egg_groups);
printf("%sTM/HM flags: 0x%hhx 0x%hhx 0x%hhx 0x%hhx 0x%hhx 0x%hhx 0x%hhx 0x%hhx\n", prefix,
stats.tm_hm_flags[0], stats.tm_hm_flags[1], stats.tm_hm_flags[2], stats.tm_hm_flags[3], stats.tm_hm_flags[4],
stats.tm_hm_flags[5], stats.tm_hm_flags[6], stats.tm_hm_flags[7]);
}
static void decodePokemonSpriteByIndex(Gen2GameReader& gameReader, uint8_t i)
{
SpriteRenderer renderer;
char fileNameBuf[50];
Gen2PokeStats stats;
uint8_t* rgbBuffer;
const char* pokeName;
const uint8_t* spriteBuffer;
uint8_t bankIndex;
uint16_t pointer;
uint8_t spriteWidthInTiles;
uint8_t spriteHeightInTiles;
uint16_t colorPalette[4];
pokeName = gameReader.getPokemonName(i);
gameReader.readPokemonStatsForIndex(i, stats);
gameReader.readSpriteDimensions(stats, spriteWidthInTiles, spriteHeightInTiles);
printf("Index %hhu:\t%s -> %hhux%hhu\n", i, pokeName, spriteWidthInTiles, spriteHeightInTiles);
printStats("\t", stats);
gameReader.readFrontSpritePointer(i, bankIndex, pointer);
// There's a bug with some sprites. (artefacts)
// it's either in the decodeSprite or drawRGB call.
// but drawRGB is also used for gen 1, where I haven't seen any artefacting.
// so it would make sense that the problem would be in decodeSprite.
// but the code there is so simple, that it doesn't make any sense for a problem to be in there.
//unless there's some memory corruption somewhere, but I haven't seen it.
spriteBuffer = gameReader.decodeSprite(bankIndex, pointer);
if(!spriteBuffer)
{
printf("Error: Could not decode sprite for %s\n", pokeName);
return;
}
gameReader.readColorPaletteForPokemon(i, false, colorPalette);
rgbBuffer = renderer.drawRGB(spriteBuffer, colorPalette, spriteWidthInTiles, spriteHeightInTiles);
snprintf(fileNameBuf, sizeof(fileNameBuf), "%s.png", pokeName);
write_png(fileNameBuf, rgbBuffer, spriteWidthInTiles * 8, spriteHeightInTiles * 8);
}
#endif
static void print_usage()
{
printf("Usage: do_stuff_gen2 <path/to/rom.gbc> <path/to/file.sav>\n");
}
int main(int argc, char** argv)
{
srand(time(NULL));
if(argc != 3)
{
print_usage();
return 1;
}
uint8_t* romBuffer;
uint8_t* savBuffer;
uint32_t romFileSize;
uint32_t savFileSize;
romBuffer = readFileIntoBuffer(argv[1], romFileSize);
if(!romBuffer)
{
fprintf(stderr, "ERROR: Couldn't read file %s\n", argv[1]);
return 1;
}
savBuffer = readFileIntoBuffer(argv[2], savFileSize);
if(!savBuffer)
{
fprintf(stderr, "ERROR: Couldn't read file %s\n", argv[2]);
free(romBuffer);
romBuffer = nullptr;
return 1;
}
GameboyCartridgeHeader cartridgeHeader;
BufferBasedRomReader romReader(romBuffer, romFileSize);
BufferBasedSaveManager saveManager(savBuffer, savFileSize);
readGameboyCartridgeHeader(romReader, cartridgeHeader);
printGameboyCartridgeHeader(cartridgeHeader);
const Gen2GameType gameType = gen2_determineGameType(cartridgeHeader);
if(gameType == Gen2GameType::INVALID)
{
fprintf(stderr, "ERROR: Not a valid Gen 2 Pokémon game!\n");
return 1;
}
Gen2GameReader gameReader(romReader, saveManager, gameType);
#if DECODE_ALL_FRONT_SPRITES
for(uint8_t i=1; i < 252; ++i)
{
decodePokemonSpriteByIndex(gameReader, i);
}
#endif
if(gameReader.isMainChecksumValid())
{
printf("Main checksum valid\n");
}
if(gameReader.isBackupChecksumValid())
{
printf("Backup checksum valid\n");
}
printf("Injecting Mew and Celebi\n");
gameReader.addDistributionPokemon(g2_clubNintendoMexico_Mew);
gameReader.addDistributionPokemon(g2_nintendoPowerCelebi);
gameReader.addDistributionPokemon(g2_pcny_mistRemoraid);
gameReader.addDistributionPokemon(g2_pcny_biteLapras);
gameReader.addDistributionPokemon(g2_pcny_shinyZapdos);
gameReader.addDistributionPokemon(g2_pcny_shinyMoltres);
gameReader.addDistributionPokemon(g2_pcny_shinyArticuno);
gameReader.addDistributionPokemon(g2_pcny_shinyEntei);
gameReader.finishSave();
if(gameReader.isMainChecksumValid())
{
printf("Main checksum valid\n");
}
if(gameReader.isBackupChecksumValid())
{
printf("Backup checksum valid\n");
}
printf("Trainer %s (0x%hx)\n", gameReader.getTrainerName(), gameReader.getTrainerID());
printf("Rival %s\n", gameReader.getRivalName());
printf("PKMN Seen: %hhu\n", gameReader.getPokedexCounter(POKEDEX_SEEN));
printf("PKMN Owned: %hhu\n", gameReader.getPokedexCounter(POKEDEX_OWNED));
printf("Party:\n");
printParty("\t", gameReader);
printBox("\t", gameReader, 0);
printBox("\t", gameReader, 1);
FILE* f = fopen("out.sav", "w");
fwrite(savBuffer, 1, savFileSize, f);
fclose(f);
free(romBuffer);
romBuffer = 0;
free(savBuffer);
savBuffer = 0;
return 0;
}

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 := ../../encodeText_gen1
# 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,27 @@
#include "gen1/Gen1Common.h"
#include <cstdio>
#include <cstring>
static void print_hex(const unsigned char* buffer, size_t length)
{
for (size_t i = 0; i < length; i++)
{
printf("%02X ", buffer[i]);
}
printf("\n");
}
int main(int argc, char** argv)
{
if(argc < 2)
{
fprintf(stderr, "Usage: encodeText_gen1 <textToEncode>\n");
return 1;
}
uint8_t outputBuffer[4096];
const uint16_t size = gen1_encodePokeText(argv[1], strlen(argv[1]), outputBuffer, sizeof(outputBuffer), 0x50);
print_hex(outputBuffer, size);
}

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 := ../../encodeText_gen2
# 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,27 @@
#include "gen2/Gen2Common.h"
#include <cstdio>
#include <cstring>
static void print_hex(const unsigned char* buffer, size_t length)
{
for (size_t i = 0; i < length; i++)
{
printf("%02X ", buffer[i]);
}
printf("\n");
}
int main(int argc, char** argv)
{
if(argc < 2)
{
fprintf(stderr, "Usage: encodeText_gen2 <textToEncode>\n");
return 1;
}
uint8_t outputBuffer[4096];
const uint16_t size = gen2_encodePokeText(argv[1], strlen(argv[1]), outputBuffer, sizeof(outputBuffer), 0x50);
print_hex(outputBuffer, size);
}

199
include/RomReader.h Normal file
View File

@ -0,0 +1,199 @@
#ifndef ROMREADER_H
#define ROMREADER_H
#include "common.h"
#include <cstdint>
class IRomReader
{
public:
virtual ~IRomReader();
/**
* @brief This function reads a byte, returns it and advances the internal pointer by 1 byte
*
* @return uint8_t
*/
virtual bool readByte(uint8_t& outByte) = 0;
/**
* @brief This function reads multiple bytes into the specified output buffer
*
* @return whether the operation was successful
*/
virtual bool read(uint8_t* outBuffer, uint32_t bytesToRead) = 0;
/**
* @brief This function keeps reading until either maxRead number of bytes have been read or the terminator has been encountered.
*/
virtual uint32_t readUntil(uint8_t* outBuffer, uint8_t terminator, uint32_t maxRead) = 0;
/**
* @brief Reads a 16 bit unsigned integer from the rom at the current position.
* The Endianness enum indicates whether the field we're trying to read is actually stored in little endian or big endian form.
* Turns out there's a mix of them in the roms even though most fields are in little endian.
*
* We need to use this value to determine whether we should do a byteswap for the CPU we're running this program on.
*/
virtual bool readUint16(uint16_t& outByte, Endianness fieldEndianness = Endianness::LITTLE) = 0;
/**
* @brief See @readUint16. This is the same but then for uint32_t
*/
virtual bool readUint32(uint32_t& outByte, Endianness fieldEndianness = Endianness::LITTLE) = 0;
/**
* @brief This function reads the current byte without advancing the internal pointer by 1 byte
*
* @return uint8_t
*/
virtual uint8_t peek() = 0;
/**
* @brief This function advances the internal pointer by the specified numBytes.
* This can be considered a relative seek
*
*/
virtual bool advance(uint32_t numBytes = 1) = 0;
/**
* @brief This function seeks to the specified absolute rom offset
*
* @param absoluteOffset
*/
virtual bool seek(uint32_t absoluteOffset) = 0;
/**
* @brief Returns the index of the current bank
*/
virtual uint8_t getCurrentBankIndex() const = 0;
/**
* @brief This function seeks to the start of the specified bank index
*
* @param bankIndex
* @return true
* @return false
*/
virtual bool seekToBank(uint8_t bankIndex) = 0;
/**
* @brief This function seeks to the specified rom pointer. Note this should be an unprocessed pointer as stored in the rom (except for endianness conversions done by readUint16)
* The reason is that the pointers stored in the rom assume the memory address of the rom location AFTER it has been mounted/mapped in the gameboy memory.
* Therefore this function will subtract 0x4000 from it (because 0x0000 - 0x4000 in gameboy address space is ALWAYS occupied by bank 0)
*
* If bankIndex is not specified (= set to 0xFF), we will seek to the pointer in the current bank
*/
virtual bool seekToRomPointer(uint16_t pointer, uint8_t bankIndex = 0xFF) = 0;
/**
* @brief This function searches for a sequence of bytes (the needle) in the buffer starting from
* the current internal position
* @return true we found the sequence of bytes and we've seeked toward this point
* @return false we didn't find the sequence of bytes anywhere
*/
virtual bool searchFor(const uint8_t* needle, uint32_t needleLength) = 0;
protected:
private:
};
class BaseRomReader : public IRomReader
{
public:
BaseRomReader();
virtual ~BaseRomReader();
bool readUint16(uint16_t& outByte, Endianness fieldEndianness = Endianness::LITTLE) override;
bool readUint32(uint32_t& outByte, Endianness fieldEndianness = Endianness::LITTLE) override;
bool seekToBank(uint8_t bankIndex) override;
bool seekToRomPointer(uint16_t pointer, uint8_t bankIndex = 0xFF) override;
uint32_t readUntil(uint8_t* outBuffer, uint8_t terminator, uint32_t maxRead) override;
protected:
private:
const bool cpuLittleEndian_;
};
class BufferBasedRomReader : public BaseRomReader
{
public:
BufferBasedRomReader(const uint8_t* buffer, uint32_t bufferSize);
virtual ~BufferBasedRomReader();
/**
* @brief This function reads a byte, returns it and advances the internal pointer by 1 byte
*
* @return uint8_t
*/
bool readByte(uint8_t& outByte) override;
/**
* @brief This function reads multiple bytes into the specified output buffer
*
* @return whether the operation was successful
*/
bool read(uint8_t* outBuffer, uint32_t bytesToRead) override;
/**
* @brief This function reads the current byte without advancing the internal pointer by 1 byte
*
* @return uint8_t
*/
uint8_t peek() override;
/**
* @brief This function advances the internal pointer by 1 byte
*
*/
bool advance(uint32_t numBytes = 1) override;
/**
* @brief This function seeks to the specified absolute rom offset
*
* @param absoluteOffset
*/
bool seek(uint32_t absoluteOffset) override;
/**
* @brief This function searches for a sequence of bytes (the needle) in the buffer starting from
* the current internal position
* @return true we found the sequence of bytes and we've seeked toward this point
* @return false we didn't find the sequence of bytes anywhere
*/
bool searchFor(const uint8_t* needle, uint32_t needleLength) override;
uint8_t getCurrentBankIndex() const override;
protected:
private:
const uint8_t* const buffer_;
const uint8_t* const end_;
const uint8_t* cur_;
};
class BitReader
{
public:
BitReader(IRomReader& romReader);
/**
* @brief Read the specified number of bits
*
* @param numBits
* @return uint8_t
*/
uint8_t read(uint8_t numBits);
bool seek(uint32_t byteOffset, uint8_t bitOffset = 0);
protected:
private:
IRomReader& romReader_;
uint8_t bitIndex_;
};
bool readGameboyCartridgeHeader(IRomReader& romReader, GameboyCartridgeHeader& cartridgeHeader);
#endif

193
include/SaveManager.h Normal file
View File

@ -0,0 +1,193 @@
#ifndef _SAVEMANAGER_H
#define _SAVEMANAGER_H
#include "common.h"
#include <cstdint>
class ISaveManager
{
public:
virtual ~ISaveManager();
/**
* @brief This function reads a byte, returns it and advances the internal pointer by 1 byte
*
* @return uint8_t
*/
virtual bool readByte(uint8_t& outByte) = 0;
virtual void writeByte(uint8_t byte) = 0;
/**
* @brief This function reads multiple bytes into the specified output buffer
*
* @return whether the operation was successful
*/
virtual bool read(uint8_t* outBuffer, uint32_t bytesToRead) = 0;
virtual void write(const uint8_t* buffer, uint32_t bytesToWrite) = 0;
/**
* @brief This function keeps reading until either maxRead number of bytes have been read or the terminator has been encountered.
*/
virtual uint32_t readUntil(uint8_t* outBuffer, uint8_t terminator, uint32_t maxRead) = 0;
/**
* @brief Reads a 16 bit unsigned integer from the SRAM at the current position.
* The Endianness enum indicates whether the field we're trying to read is actually stored in little endian or big endian form.
* Turns out there's a mix of them in the rom/SRAM even though most fields are in little endian.
*
* We need to use this value to determine whether we should do a byteswap for the CPU we're running this program on.
*/
virtual bool readUint16(uint16_t& outBytes, Endianness fieldEndianness = Endianness::LITTLE) = 0;
/**
* @brief Writes a 16 bit unsigned integer to the SRAM at the current position.
* The Endianness enum indicates whether the field we're trying to read is actually stored in little endian or big endian form.
* Turns out there's a mix of them in the rom/SRAM even though most fields are in little endian.
*
* We need to use this value to determine whether we should do a byteswap for the CPU we're running this program on.
*/
virtual void writeUint16(uint16_t bytes, Endianness fieldEndianness = Endianness::LITTLE) = 0;
/**
* @brief See @readUint16. This is the same but then for reading 3 bytes and returning it as 32 bit uint
*/
virtual bool readUint24(uint32_t& outBytes, Endianness fieldEndianness = Endianness::LITTLE) = 0;
/**
* @brief See @writeUint16. This is the same, but for writing 3 bytes in the right endianness for the SRAM
*/
virtual void writeUint24(uint32_t bytes, Endianness fieldEndianness = Endianness::LITTLE) = 0;
/**
* @brief See @readUint16. This is the same but then for uint32_t
*/
virtual bool readUint32(uint32_t& outBytes, Endianness fieldEndianness = Endianness::LITTLE) = 0;
/**
* @brief This function reads the current byte without advancing the internal pointer by 1 byte
*
* @return uint8_t
*/
virtual uint8_t peek() = 0;
/**
* @brief This function advances the internal pointer by the specified numBytes.
* This can be considered a relative seek
*
*/
virtual bool advance(uint32_t numBytes = 1) = 0;
/**
* @brief This function rewinds the internal pointer with the specified numBytes
* This can be considered a backward relative seek
*/
virtual bool rewind(uint32_t numBytes = 1) = 0;
/**
* @brief This function seeks to the specified absolute SRAM offset
*
* @param absoluteOffset
*/
virtual bool seek(uint32_t absoluteOffset) = 0;
/**
* @brief Returns the index of the current bank
*/
virtual uint8_t getCurrentBankIndex() const = 0;
/**
* @brief This function seeks to the start of the specified bank index
*/
virtual bool seekToBank(uint8_t bankIndex) = 0;
/**
* @brief This function seeks to the specified offset in the specified save bank.
*/
virtual bool seekToBankOffset(uint8_t bankIndex, uint16_t offset) = 0;
/**
* @brief Copies a region from the save to another region.
*/
virtual void copyRegion(uint32_t absoluteSrcOffset, uint32_t absoluteDstOffsetStart, uint32_t numBytes) = 0;
protected:
private:
};
class BaseSaveManager : public ISaveManager
{
public:
BaseSaveManager();
virtual ~BaseSaveManager();
bool readUint16(uint16_t& outBytes, Endianness fieldEndianness = Endianness::LITTLE) override;
void writeUint16(uint16_t bytes, Endianness fieldEndianness = Endianness::LITTLE) override;
bool readUint24(uint32_t& outBytes, Endianness fieldEndianness = Endianness::LITTLE) override;
void writeUint24(uint32_t bytes, Endianness fieldEndianness = Endianness::LITTLE) override;
bool readUint32(uint32_t& outBytes, Endianness fieldEndianness = Endianness::LITTLE) override;
bool seekToBank(uint8_t bankIndex) override;
bool seekToBankOffset(uint8_t bankIndex, uint16_t offset);
uint32_t readUntil(uint8_t* outBuffer, uint8_t terminator, uint32_t maxRead) override;
void copyRegion(uint32_t absoluteSrcOffset, uint32_t absoluteDstOffsetStart, uint32_t absoluteDstOffsetEnd) override;
protected:
private:
const bool cpuLittleEndian_;
};
class BufferBasedSaveManager : public BaseSaveManager
{
public:
BufferBasedSaveManager(uint8_t* buffer, uint32_t bufferSize);
virtual ~BufferBasedSaveManager();
/**
* @brief This function reads a byte, returns it and advances the internal pointer by 1 byte
*
* @return uint8_t
*/
bool readByte(uint8_t& outByte) override;
virtual void writeByte(uint8_t byte) override;
/**
* @brief This function reads multiple bytes into the specified output buffer
*
* @return whether the operation was successful
*/
bool read(uint8_t* outBuffer, uint32_t bytesToRead) override;
void write(const uint8_t* buffer, uint32_t bytesToWrite) override;
/**
* @brief This function reads the current byte without advancing the internal pointer by 1 byte
*
* @return uint8_t
*/
uint8_t peek() override;
/**
* @brief This function advances the internal pointer by the specified numBytes.
* This can be considered a relative seek
*
*/
bool advance(uint32_t numBytes = 1) override;
bool rewind(uint32_t numBytes = 1) override;
/**
* @brief This function seeks to the specified absolute save file/buffer offset
*
* @param absoluteOffset
*/
bool seek(uint32_t absoluteOffset) override;
/**
* @brief Returns the index of the current bank
*/
uint8_t getCurrentBankIndex() const override;
protected:
private:
uint8_t* const buffer_;
uint8_t* const end_;
uint8_t* cur_;
};
#endif

31
include/SpriteRenderer.h Normal file
View File

@ -0,0 +1,31 @@
#ifndef _SPRITERENDERER_H
#define _SPRITERENDERER_H
#include <cstdint>
#define MAX_SPRITE_TILES_WIDTH 7
#define MAX_SPRITE_PIXEL_WIDTH MAX_SPRITE_TILES_WIDTH * 8
#define MAX_SPRITE_TILES_HEIGHT 7
#define MAX_SPRITE_PIXEL_HEIGHT MAX_SPRITE_TILES_HEIGHT * 8
class SpriteRenderer
{
public:
/**
* Returns the number of RGB bytes to store RGB values for a sprite with the given number of horizontal and vertical tiles
*/
uint32_t getNumRGBBytesFor(uint8_t numHTiles, uint8_t numVTiles) const;
/**
* @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.
* You don't need to free() the data.
*/
uint8_t* drawRGB(const uint8_t* spriteBuffer, uint16_t palette[4], uint8_t numHTiles, uint8_t numVTiles);
protected:
private:
uint8_t rgbBuffer_[MAX_SPRITE_PIXEL_WIDTH * MAX_SPRITE_PIXEL_HEIGHT * 3];
};
#endif

94
include/common.h Normal file
View File

@ -0,0 +1,94 @@
#ifndef _COMMON_H
#define _COMMON_H
#include <cstdint>
enum class Endianness
{
LITTLE,
BIG
};
typedef struct GameboyCartridgeHeader
{
uint8_t entry[4];
char title[17];
char manufacturer_code[5];
uint8_t CGB_flag;
char new_licensee_code[2];
uint8_t SGB_flag;
uint8_t cartridge_type;
uint8_t rom_size;
uint8_t ram_size;
uint8_t destination_code;
uint8_t old_licensee_code;
uint8_t mask_rom_version_number;
uint8_t header_checksum;
uint8_t global_checksum[2];
} GameboyCartridgeHeader;
enum class PokeStat
{
HP,
ATK,
DEF,
SPEED,
SPECIAL,
SPECIAL_ATK,
SPECIAL_DEF
};
enum GrowthRate
{
GRW_MEDIUM_FAST = 0,
GRW_SLIGHTLY_FAST = 1,
GRW_SLIGHTLY_SLOW = 2,
GRW_MEDIUM_SLOW = 3,
GRW_FAST = 4,
GRW_SLOW = 5
};
enum PokedexFlag
{
POKEDEX_SEEN,
POKEDEX_OWNED
};
typedef struct TextCodePair
{
uint8_t code;
const char* chars;
} TextCodePair;
extern uint16_t monochromeGBColorPalette[4];
/**
* converts the given gameboy color palette to RGB values.
* Will always return a uint8_t[12] array.
*
* Note: the next call to this function overwrites the values in the returned pointer
*/
uint8_t* convertGBColorPaletteToRGB24(uint16_t palette[4]);
/**
* @brief This function gets the specific IV value for the given PokeStat type
*/
uint8_t getStatIV(PokeStat type, const uint8_t iv_data[2]);
/**
* @brief This function calculates the given PokeStat type using the given parameters.
*
* @type the PokeStat type to calculate
* @baseVal the base value for the given pokemon
* @stat_iv the stat iv obtained using getStatIV()
*
*/
uint16_t calculatePokeStat(PokeStat type, uint16_t baseVal, uint8_t stat_iv, uint16_t stat_exp, uint8_t level);
uint32_t *getExpTableForGrowthRate(uint8_t growthRate);
uint32_t getExpNeededForLevel(uint8_t level, uint8_t growthRate);
uint8_t getLevelForExp(uint32_t exp, uint8_t growthRate);
void printGameboyCartridgeHeader(const GameboyCartridgeHeader& cartridgeHeader);
#endif

121
include/gen1/Gen1Common.h Normal file
View File

@ -0,0 +1,121 @@
#ifndef _GEN1COMMON_H
#define _GEN1COMMON_H
#include "common.h"
class Gen1GameReader;
enum class Gen1GameType
{
INVALID,
BLUE,
RED,
YELLOW
};
typedef struct Gen1PokeStats
{
uint8_t pokedex_number;
uint8_t base_hp;
uint8_t base_attack;
uint8_t base_defense;
uint8_t base_speed;
uint8_t base_special;
uint8_t type1;
uint8_t type2;
uint8_t catch_rate;
uint8_t base_exp_yield;
uint8_t front_sprite_dimensions;
uint8_t sprite_bank;
uint16_t pointer_to_frontsprite;
uint16_t pointer_to_backsprite;
uint8_t lvl1_attacks[4];
uint8_t growth_rate;
uint8_t tm_hm_flags[7];
} Gen1PokeStats;
typedef struct Gen1TrainerPokemon
{
uint8_t poke_index;
uint16_t current_hp;
uint8_t level;
uint8_t status_condition;
uint8_t type1;
uint8_t type2;
uint8_t catch_rate_or_held_item;
uint8_t index_move1;
uint8_t index_move2;
uint8_t index_move3;
uint8_t index_move4;
uint16_t original_trainer_ID;
uint32_t exp;
uint16_t hp_effort_value;
uint16_t atk_effort_value;
uint16_t def_effort_value;
uint16_t speed_effort_value;
uint16_t special_effort_value;
uint8_t iv_data[2];
uint8_t pp_move1;
uint8_t pp_move2;
uint8_t pp_move3;
uint8_t pp_move4;
uint16_t max_hp;
uint16_t atk;
uint16_t def;
uint16_t speed;
uint16_t special;
} Gen1TrainerPokemon;
enum Gen1PokeType
{
GEN1_PT_NORMAL = 0x0,
GEN1_PT_FIGHTING,
GEN1_PT_FLYING,
GEN1_PT_POISON,
GEN1_PT_GROUND,
GEN1_PT_ROCK,
GEN1_PT_BIRD,
GEN1_PT_BUG,
GEN1_PT_GHOST,
GEN1_PT_FIRE = 0x14,
GEN1_PT_WATER = 0x15,
GEN1_PT_GRASS = 0x16,
GEN1_PT_ELECTRIC = 0x17,
GEN1_PT_PSYCHIC = 0x18,
GEN1_PT_ICE = 0x19,
GEN1_PT_DRAGON = 0x1A
};
/**
* @brief Implementation based on https://bulbapedia.bulbagarden.net/wiki/Save_data_structure_(Generation_I)#Checksum
*/
class Gen1Checksum
{
public:
Gen1Checksum();
void addByte(uint8_t byte);
uint8_t get() const;
protected:
private:
uint8_t checksum_;
};
/**
* @brief Determines a Gen1GameType based on the given GameboyCartridgeHeader struct
*/
Gen1GameType gen1_determineGameType(const GameboyCartridgeHeader& cartridgeHeader);
void gen1_recalculatePokeStats(Gen1GameReader& gameReader, Gen1TrainerPokemon& poke);
/**
* @brief This function decodes a text (This could be a name or something else) found in the rom.
* @return the number of characters copied to the output buffer
*/
uint16_t gen1_decodePokeText(const uint8_t* inputBuffer, uint16_t inputBufferLength, char* outputBuffer, uint16_t outputBufferLength);
/**
* @brief The opposite of gen1_decodePokeText()
*/
uint16_t gen1_encodePokeText(const char* inputBuffer, uint16_t inputBufferLength, uint8_t* outputBuffer, uint16_t outputBufferLength, uint8_t terminator);
#endif

View File

@ -0,0 +1,63 @@
#ifndef GEN1DISTRIBUTIONPOKEMON_H
#define GEN1DISTRIBUTIONPOKEMON_H
#include "Gen1Common.h"
typedef struct Gen1DistributionPokemon
{
const char* name;
// name of the original trainer as it came from the distribution machine or "" if irrelevant
const char* originalTrainer;
// if regenerateTrainerID = false, use it as is
// if regenerateTrainerID = true, use it as max bounds or 0 if no bounds
uint16_t originalTrainerID;
// this iv_data will override the iv_data that is inside the poke member.
// this is because we might have different distributions with slight differences of the same pokemon
uint8_t iv_data[2];
// set the player name as the original trainer ID
bool setPlayerAsOriginalTrainer;
// generate a random trainer ID for the pokemon. see originalTrainerID above
bool regenerateTrainerID;
// definition of the pokemon
Gen1TrainerPokemon poke;
} Gen1DistributionPokemon;
// https://bulbapedia.bulbagarden.net/wiki/List_of_European_language_event_Pok%C3%A9mon_distributions_(Generation_I)
extern const Gen1DistributionPokemon g1_nintendoUKPokemonFestivalMew2016;
extern const Gen1DistributionPokemon g1_clubNintendoMexicoGiveawayMew;
extern const Gen1DistributionPokemon g1_farMorBornMew;
extern const Gen1DistributionPokemon g1_christmasPresentMew;
extern const Gen1DistributionPokemon g1_swedenMewOnTour;
extern const Gen1DistributionPokemon g1_tennispalatsiMew;
extern const Gen1DistributionPokemon g1_norwayPokemonTourMew;
extern const Gen1DistributionPokemon g1_clubNintendoMew;
extern const Gen1DistributionPokemon g1_pokemon2000ChampionshipMew;
extern const Gen1DistributionPokemon g1_millenniumDomeMew;
extern const Gen1DistributionPokemon g1_austriaMew;
extern const Gen1DistributionPokemon g1_ukIrelandPokemonChampionship2000Mew;
extern const Gen1DistributionPokemon g1_coraChatelineauMew;
extern const Gen1DistributionPokemon g1_francePokemonTournamentMew;
extern const Gen1DistributionPokemon g1_spainPokemonTournamentMew;
extern const Gen1DistributionPokemon g1_mewsFlashMew;
extern const Gen1DistributionPokemon g1_pokemonPatrolMew;
extern const Gen1DistributionPokemon g1_nintendoOfficialMagazineTourMew;
extern const Gen1DistributionPokemon g1_canadianPokemonStadiumTour2000Mew;
extern const Gen1DistributionPokemon g1_pokemonStadiumTour2000Mew;
extern const Gen1DistributionPokemon g1_canadaToysRUsMew;
extern const Gen1DistributionPokemon g1_usToysRUsMew;
extern const Gen1DistributionPokemon g1_nintendoPowerMew;
extern const Gen1DistributionPokemon g1_pokemonLeagueNintendoTrainingTour99Mew;
extern const Gen1DistributionPokemon g1_nintendoPowerPikachu;
extern const Gen1DistributionPokemon g1_pokeTourMew;
extern const Gen1DistributionPokemon g1_pokemonPowerMew;
/**
* @brief Gets a list of const Gen1DistributionPokemon pointers of the distribution pokemon that were available worldwide
* and stores it in the specified outList.
*
* outSize will be updated with the size of the list
*/
void gen1_getMainDistributionPokemonList(const Gen1DistributionPokemon**& outList, uint32_t& outSize);
#endif

View File

@ -0,0 +1,174 @@
#ifndef GEN1GAMEREADER_H
#define GEN1GAMEREADER_H
#include "gen1/Gen1SpriteDecoder.h"
#include "gen1/Gen1PlayerPokemonStorage.h"
#include "gen1/Gen1DistributionPokemon.h"
class IRomReader;
class ISaveManager;
class Gen1GameReader
{
public:
Gen1GameReader(IRomReader& romReader, ISaveManager& saveManager, Gen1GameType gameType);
/**
* @brief get the name of a pokémon based on an index number
* Note: you don't own the returned pointer. The data will get overwritten on the next call to this function,
* so make sure to strdup() it if you need to store it for later
*
* @return const char*
*/
const char* getPokemonName(uint8_t index) const;
/**
* @brief Gets the pokedex number of a pokemon at the given internal index
*/
uint8_t getPokemonNumber(uint8_t index) const;
/**
* @brief With this function, you can check if the index is valid and not referring to missingno
*/
bool isValidIndex(uint8_t index) const;
/**
* @brief This function reads the pokemon stats at the given index
*
* @return whether or not this operation was successful
*/
bool readPokemonStatsForIndex(uint8_t index, Gen1PokeStats& stats) const;
/**
* @brief Get the index of the color palette based on the pokedex number of a certain pokemon
*/
uint8_t getColorPaletteIndexByPokemonNumber(uint8_t pokeNumber);
/**
* @brief This function reads the color palette with the given ID into outColorPalette
* outColorPalette should be a pointer to an uint16_t array with 4 elements
*
* Based on: https://bulbapedia.bulbagarden.net/wiki/List_of_color_palettes_by_index_number_(Generation_I)
*
* @param outColorPalette
*/
void readColorPalette(uint8_t paletteId, uint16_t* outColorPalette);
/**
* @brief Get the trainer name from the save file
* Note: the resulting const char* does not need to be free'd.
* However, it needs to be either used rightaway or strdup()'d, because the data will get overwritten on the next call to this function.
*/
const char* getTrainerName() const;
/**
* @brief Get the rival name from the save file
* Note: the resulting const char* does not need to be free'd.
* However, it needs to be either used rightaway or strdup()'d, because the data will get overwritten on the next call to this function.
*/
const char* getRivalName() const;
/**
* @brief Get the player ID from the save file
*/
uint16_t getTrainerID() const;
/**
* @brief Returns a Gen1Party instance, which can be used to retrieve the information about the pokemon currently in the trainers' party
*/
Gen1Party getParty();
/**
* @brief Returns a Gen1Box instance, which can be used to retrieve information about the pokemon currently in the Trainers' PC box at the specified index
*/
Gen1Box getBox(uint8_t boxIndex);
/**
* @brief Returns the 0-based index of the currently selected PC box for pokemon
*/
uint8_t getCurrentBoxIndex();
/**
* @brief This function returns whether the pokemon with the given pokedexNumber has been SEEN/OWNED.
*/
bool getPokedexFlag(PokedexFlag dexFlag, uint8_t pokedexNumber) const;
/**
* @brief This function sets the pokedex flag for the given pokedexNumber
*/
void setPokedexFlag(PokedexFlag dexFlag, uint8_t pokedexNumber) const;
/**
* @brief This function returns the counter value of the given dex flag
*/
uint8_t getPokedexCounter(PokedexFlag dexFlag) const;
/**
* @brief This function decodes a sprite at the given bank and pointer.
* Returns a pointer to a buffer containing the decoded gameboy sprite in gameboy sprite format.
* The size of the returned buffer is SPRITE_BITPLANE_BUFFER_SIZE_IN_BYTES * 2
*
* WARNING: this function returns a pointer to an internal buffer of the Gen1SpriteDecoder. 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* decodeSprite(uint8_t bankIndex, uint16_t pointer);
/**
* @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
*
* @param originalTrainerID optional parameter. If not specified (=null), the original trainer id string will be set to the players'
* @param nickname optional parameter. If not specified (= null), the pokemon species name will be used instead.
*
* @return
* 0xFF - Could not add pokemon (probably no space left)
* 0xFE - Added to players' party
* 0x0 - 0xB - Added to box at index <value>
*/
uint8_t addPokemon(Gen1TrainerPokemon& poke, const char* originalTrainerID = 0, const char* nickname = 0);
/**
* @brief Adds a distribution 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
*
* @param nickname optional parameter. If not specified (= null), the pokemon species name will be used instead.
*
* @return
* 0xFF - Could not add pokemon (probably no space left)
* 0xFE - Added to players' party
* 0x0 - 0xB - Added to box at index <value>
*/
uint8_t addDistributionPokemon(const Gen1DistributionPokemon& distributionPoke, const char* nickname = 0);
/**
* @brief This function checks whether the main checksum of bank 1 is valid
*/
bool isMainChecksumValid();
/**
* @brief Updates the main data checksum in bank 1
*/
void updateMainChecksum();
/**
* @brief Checks whether the whole bank checksum of the given memory bank is valid.
* As you may or may not know, bank 2 and bank 3 contain the PC Pokemon boxes of the player
* These memory banks have 2 types of checksums: a whole memory bank checksum (which is this one)
* And a checksum per box. (which will be dealt with in the Gen1Box class)
*
* So yeah, this function is about the whole memory bank checksums in memory bank 2 and 3
*/
bool isWholeBoxBankValid(uint8_t bankIndex);
/**
* @brief This function updates the checksum value of the whole box bank (bank 2 or 3)
*/
void updateWholeBoxBankChecksum(uint8_t bankIndex);
protected:
private:
IRomReader& romReader_;
ISaveManager& saveManager_;
Gen1SpriteDecoder spriteDecoder_;
Gen1GameType gameType_;
};
#endif

View File

@ -0,0 +1,61 @@
#ifndef GEN1_PLAYERPOKEMONSTORAGE_H
#define GEN1_PLAYERPOKEMONSTORAGE_H
#include "gen1/Gen1Common.h"
class Gen1GameReader;
class ISaveManager;
uint8_t getGen1BoxBankIndex(uint8_t boxIndex, uint8_t currentBoxIndex);
/**
* @brief This class represents the trainer party in the GEN1 games
*/
class Gen1Party
{
public:
Gen1Party(Gen1GameReader& gameReader, ISaveManager& saveManager);
~Gen1Party();
bool getPokemon(uint8_t index, Gen1TrainerPokemon& outTrainerPokemon);
uint8_t getNumberOfPokemon();
uint8_t getMaxNumberOfPokemon();
const char* getPokemonNickname(uint8_t index);
void setPokemonNickname(uint8_t index, const char* nickname = 0);
const char* getOriginalTrainerOfPokemon(uint8_t index);
void setOriginalTrainerOfPokemon(uint8_t index, const char* originalTrainer);
bool add(Gen1TrainerPokemon& poke, const char* originalTrainerID, const char* nickname = 0);
protected:
private:
Gen1GameReader& gameReader_;
ISaveManager& saveManager_;
};
/**
* @brief This class represents a PC Box of the player in the GEN1 games
*/
class Gen1Box
{
public:
Gen1Box(Gen1GameReader& gameReader, ISaveManager& saveManager, uint8_t boxIndex);
~Gen1Box();
bool getPokemon(uint8_t index, Gen1TrainerPokemon& outTrainerPokemon);
uint8_t getNumberOfPokemon();
uint8_t getMaxNumberOfPokemon();
const char* getPokemonNickname(uint8_t index);
void setPokemonNickname(uint8_t index, const char* nickname = 0);
const char* getOriginalTrainerOfPokemon(uint8_t index);
void setOriginalTrainerOfPokemon(uint8_t index, const char* originalTrainer);
bool add(Gen1TrainerPokemon& poke, const char* originalTrainerID, const char* nickname = 0);
bool isChecksumValid(uint8_t currentBoxIndex);
void updateChecksum(uint8_t currentBoxIndex);
protected:
private:
Gen1GameReader& gameReader_;
ISaveManager& saveManager_;
uint8_t boxIndex_;
};
#endif

View File

@ -0,0 +1,43 @@
#ifndef GEN1SPRITEDECODER_H
#define GEN1SPRITEDECODER_H
#include <cstdint>
#define SPRITE_BUFFER_WIDTH_IN_PIXELS 56
#define SPRITE_BUFFER_HEIGHT_IN_PIXELS 56
#define SPRITE_BITPLANE_BUFFER_SIZE_IN_BYTES ((SPRITE_BUFFER_WIDTH_IN_PIXELS * SPRITE_BUFFER_HEIGHT_IN_PIXELS) / 8)
#define GEN1_DECODED_SPRITE_BUFFER_SIZE_IN_BYTES SPRITE_BITPLANE_BUFFER_SIZE_IN_BYTES * 2
class IRomReader;
/**
* @brief based on https://www.youtube.com/watch?v=aF1Yw_wu2cM and https://www.youtube.com/watch?v=ZI50XUeN6QE
*
*/
class Gen1SpriteDecoder
{
public:
Gen1SpriteDecoder(IRomReader& romReader);
~Gen1SpriteDecoder();
/**
* @brief This function decodes the sprite at the specified bank and pointer
* and returns a pointer to the decoded sprite buffer.
*
* NOTE: this decoded sprite may or may not be what you expect. The format of it is described here:
* https://rgmechex.com/tech/gen1decompress.html and as the function level comment of mergeSpriteBitplanes() in the .cpp file
*
* Careful: the returned buffer is reused/recycled on the next call to decode().
* So if you want to keep the data around after the next decode() call, you should make a copy of it.
*
* The size of the returned buffer is SPRITE_BITPLANE_BUFFER_SIZE_IN_BYTES * 2
*/
uint8_t* decode(uint8_t bankIndex, uint16_t pointer);
protected:
private:
IRomReader& romReader_;
// 392 bytes per buffer (56x56 pixels at 1 bit per pixel) (1 tile is 8x8)
uint8_t bigSpriteBuffer_[SPRITE_BITPLANE_BUFFER_SIZE_IN_BYTES * 3];
};
#endif

193
include/gen2/Gen2Common.h Normal file
View File

@ -0,0 +1,193 @@
#ifndef _GEN2COMMON_H
#define _GEN2COMMON_H
#include "common.h"
class Gen2GameReader;
class ISaveManager;
enum class Gen2GameType
{
INVALID,
GOLD,
SILVER,
CRYSTAL
};
typedef struct Gen2PokeStats
{
uint8_t pokedex_number;
uint8_t base_hp;
uint8_t base_attack;
uint8_t base_defense;
uint8_t base_speed;
uint8_t base_special_attack;
uint8_t base_special_defense;
uint8_t type1;
uint8_t type2;
uint8_t catch_rate;
uint8_t base_exp_yield;
uint8_t wild_held_item1;
uint8_t wild_held_item2;
uint8_t gender_ratio;
uint8_t unknown;
uint8_t egg_cycles;
uint8_t unknown2;
uint8_t front_sprite_dimensions;
uint8_t blank[4];
uint8_t growth_rate;
uint8_t egg_groups;
uint8_t tm_hm_flags[8];
} Gen2PokeStats;
enum Gen2PokeType
{
GEN2_PT_NORMAL = 0x0,
GEN2_PT_FIGHTING,
GEN2_PT_FLYING,
GEN2_PT_POISON,
GEN2_PT_GROUND,
GEN2_PT_ROCK,
GEN2_PT_BIRD,
GEN2_PT_BUG,
GEN2_PT_GHOST,
GEN2_PT_STEEL,
GEN2_PT_FIRE = 0x14,
GEN2_PT_WATER = 0x15,
GEN2_PT_GRASS = 0x16,
GEN2_PT_ELECTRIC = 0x17,
GEN2_PT_PSYCHIC = 0x18,
GEN2_PT_ICE = 0x19,
GEN2_PT_DRAGON = 0x1A,
GEN2_PT_DARK = 0x1B
};
enum Gen2GrowthRate
{
GEN2_GRW_MEDIUM_FAST = 0,
GEN2_GRW_SLIGHTLY_FAST = 1,
GEN2_GRW_SLIGHTLY_SLOW = 2,
GEN2_GRW_MEDIUM_SLOW = 3,
GEN2_GRW_FAST = 4,
GEN2_GRW_SLOW = 5
};
enum Gen2ItemListType
{
GEN2_ITEMLISTTYPE_ITEMPOCKET,
GEN2_ITEMLISTTYPE_KEYITEMPOCKET,
GEN2_ITEMLISTTYPE_BALLPOCKET,
GEN2_ITEMLISTTYPE_PC
};
typedef struct Gen2TrainerPokemon
{
uint8_t poke_index;
uint8_t held_item_index;
uint8_t index_move1;
uint8_t index_move2;
uint8_t index_move3;
uint8_t index_move4;
uint16_t original_trainer_ID;
uint32_t exp;
uint16_t hp_effort_value;
uint16_t atk_effort_value;
uint16_t def_effort_value;
uint16_t speed_effort_value;
uint16_t special_effort_value;
uint8_t iv_data[2];
uint8_t pp_move1;
uint8_t pp_move2;
uint8_t pp_move3;
uint8_t pp_move4;
uint8_t friendship_or_remaining_egg_cycles;
uint8_t pokerus;
uint16_t caught_data;
uint8_t level;
uint8_t status_condition;
uint8_t unused_byte;
uint16_t current_hp;
uint16_t max_hp;
uint16_t atk;
uint16_t def;
uint16_t speed;
uint16_t special_atk;
uint16_t special_def;
} Gen2TrainerPokemon;
class Gen2ItemList
{
public:
Gen2ItemList(ISaveManager& saveManager, Gen2ItemListType type, bool isCrystal);
uint8_t getCount();
uint8_t getCapacity();
/**
* Gets the entry info at the specified index in the item list
*/
bool getEntry(uint8_t index, uint8_t& outItemId, uint8_t& outCount);
/**
* @brief Gets the next entry's info in the item list.
* The reason this exists is to avoid the seek that would be done on every call to getEntry()
*
* If the previous function call doing stuff on saveManager_ was the getCount() function, you can
* bypass a call to getEntry() and call this getNextEntry() function immediately
*/
bool getNextEntry(uint8_t& outItemId, uint8_t& outCount);
/**
* @brief This function adds an item (or multiple of that specific item) to the item list and returns true if this was successful
* WARNING: You must call finishSave() on the Gen2GameReader after calling this function. Not doing so will corrupt your save file!
*/
bool add(uint8_t itemId, uint8_t itemCount);
protected:
private:
bool seekToBasePos();
ISaveManager& saveManager_;
const Gen2ItemListType type_;
const bool isCrystal_;
};
class Gen2Checksum
{
public:
Gen2Checksum();
void addByte(uint8_t byte);
uint16_t get() const;
protected:
private:
uint16_t checksum_;
};
/**
* @brief Determines a Gen2GameType based on the given GameboyCartridgeHeader struct
*/
Gen2GameType gen2_determineGameType(const GameboyCartridgeHeader& cartridgeHeader);
void gen2_recalculatePokeStats(Gen2GameReader& gameReader, Gen2TrainerPokemon& poke);
/**
* @brief This function decodes a text (This could be a name or something else) found in the rom.
* @return the number of characters copied to the output buffer
*/
uint16_t gen2_decodePokeText(const uint8_t* inputBuffer, uint16_t inputBufferLength, char* outputBuffer, uint16_t outputBufferLength);
/**
* @brief The opposite of gen1_decodePokeText()
*/
uint16_t gen2_encodePokeText(const char* inputBuffer, uint16_t inputBufferLength, uint8_t* outputBuffer, uint16_t outputBufferLength, uint8_t terminator);
/**
* @brief This function determines whether the given trainer pokemon is shiny
*/
bool gen2_isPokemonShiny(Gen2TrainerPokemon &poke);
/**
* @brief This function makes the necessary IV changes to make the pokemon shiny
*/
void gen2_makePokemonShiny(Gen2TrainerPokemon& poke);
#endif

View File

@ -0,0 +1,212 @@
#ifndef _GEN2DISTRIBUTIONPOKEMON_H
#define _GEN2DISTRIBUTIONPOKEMON_H
#include "gen2/Gen2Common.h"
typedef struct Gen2DistributionPokemon
{
const char* name;
// name of the original trainer as it came from the distribution machine or "" if irrelevant
// ignored if setPlayerAsOriginalTrainer == true
const char* originalTrainer;
// if regenerateTrainerID = false, use it as is
// if regenerateTrainerID = true, use it as max bounds or 0 if no bounds
// ignored if setPlayerAsOriginalTrainer == true
uint16_t originalTrainerID;
// set the player name as the original trainer ID
bool setPlayerAsOriginalTrainer;
// generate a random trainer ID for the pokemon. see originalTrainerID above
// ignored if setPlayerAsOriginalTrainer == true
bool regenerateTrainerID;
// value between 0-100 that indicates the chance for a shiny variant
// if the value is set to 0xFF, this field should be ignored
// this is mostly used for simulating the chances of the PKCNY pokemon
uint8_t shinyChance;
// a level to override the default level in Gen2TrainerPokemon with.
// if set to 0, this field should be ignored
uint8_t overrideLevel;
// value indicates that this pokémon is given as an egg
bool isEgg;
// this iv_data will override the iv_data that is inside the poke member.
// this is because we might have different distributions with slight differences of the same pokemon
// ignored if randomizeIVs == true
// could also randomly be ignored if shinyChance != 0xFF
uint8_t iv_data[2];
// this value indicates whether the iv_data field above should be used
// or whether the IVs need to be randomized instead
// shinyChance still applies here if set to a value != 0xFF
bool randomizeIVs;
// definition of the pokemon
Gen2TrainerPokemon poke;
} Gen2DistributionPokemon;
// Note: see gen2_getMainDistributionPokemonList() and gen2_getPokemonCenterNewYorkDistributionPokemonList() below
extern const Gen2DistributionPokemon g2_clubNintendoMexico_Mew;
extern const Gen2DistributionPokemon g2_clubNintendoMexico_Celebi;
extern const Gen2DistributionPokemon g2_swedenCelebi;
extern const Gen2DistributionPokemon g2_westfieldShopping;
extern const Gen2DistributionPokemon g2_celebiTour;
extern const Gen2DistributionPokemon g2_pokemonFunFestCelebi;
extern const Gen2DistributionPokemon g2_nintendoPowerCelebi;
extern const Gen2DistributionPokemon g2_celebiSweepstakes;
// PCNY (Pokemon Center New York) Gotta Catch 'em all! station distribution pokémon below
extern const Gen2DistributionPokemon g2_pcny_ancientpowerBulbasaur;
extern const Gen2DistributionPokemon g2_pcny_shinyVenusaur;
extern const Gen2DistributionPokemon g2_pcny_crunchCharmander;
extern const Gen2DistributionPokemon g2_pcny_shinyCharizard;
extern const Gen2DistributionPokemon g2_pcny_zapCannonSquirtle;
extern const Gen2DistributionPokemon g2_pcny_shinyBlastoise;
extern const Gen2DistributionPokemon g2_pcny_sonicboomSpearow;
extern const Gen2DistributionPokemon g2_pcny_lovelyKissNidoranFem;
extern const Gen2DistributionPokemon g2_pcny_moonlightNidoranFem;
extern const Gen2DistributionPokemon g2_pcny_sweetKissNidoranFem;
extern const Gen2DistributionPokemon g2_pcny_lovelyKissNidoranMale;
extern const Gen2DistributionPokemon g2_pcny_morningSunNidoranMale;
extern const Gen2DistributionPokemon g2_pcny_sweetKissNidoranMale;
extern const Gen2DistributionPokemon g2_pcny_flailZubat;
extern const Gen2DistributionPokemon g2_pcny_leechSeedOddish;
extern const Gen2DistributionPokemon g2_pcny_synthesisParas;
extern const Gen2DistributionPokemon g2_pcny_petalDancePsyduck;
extern const Gen2DistributionPokemon g2_pcny_triAttackPsyduck;
extern const Gen2DistributionPokemon g2_pcny_growthPoliwag;
extern const Gen2DistributionPokemon g2_pcny_lovelyKissPoliwag;
extern const Gen2DistributionPokemon g2_pcny_sweetKissPoliwag;
extern const Gen2DistributionPokemon g2_pcny_foresightAbra;
extern const Gen2DistributionPokemon g2_pcny_falseSwipeMachop;
extern const Gen2DistributionPokemon g2_pcny_thrashMachop;
extern const Gen2DistributionPokemon g2_pcny_lovelyKissBellsprout;
extern const Gen2DistributionPokemon g2_pcny_sweetKissBellsprout;
extern const Gen2DistributionPokemon g2_pcny_confuseRayTentacool;
extern const Gen2DistributionPokemon g2_pcny_rapidSpinGeodude;
extern const Gen2DistributionPokemon g2_pcny_lowKickPonyta;
extern const Gen2DistributionPokemon g2_pcny_agilityMagnemite;
extern const Gen2DistributionPokemon g2_pcny_furyCutterFarfetchd;
extern const Gen2DistributionPokemon g2_pcny_lowKickDoduo;
extern const Gen2DistributionPokemon g2_pcny_flailSeel;
extern const Gen2DistributionPokemon g2_pcny_sharpenOnix;
extern const Gen2DistributionPokemon g2_pcny_amnesiaDrowsee;
extern const Gen2DistributionPokemon g2_pcny_metalClawKrabby;
extern const Gen2DistributionPokemon g2_pcny_agilityVoltorb;
extern const Gen2DistributionPokemon g2_pcny_sweetScentExeggcute;
extern const Gen2DistributionPokemon g2_pcny_furyAttackCubone;
extern const Gen2DistributionPokemon g2_pcny_doubleSlapLickitung;
extern const Gen2DistributionPokemon g2_pcny_sweetScentChansey;
extern const Gen2DistributionPokemon g2_pcny_synthesisTangela;
extern const Gen2DistributionPokemon g2_pcny_faintAttackKangaskhan;
extern const Gen2DistributionPokemon g2_pcny_hazeHorsea;
extern const Gen2DistributionPokemon g2_pcny_swordsDanceGoldeen;
extern const Gen2DistributionPokemon g2_pcny_twisterStaryu;
extern const Gen2DistributionPokemon g2_pcny_mindReaderMrMime;
extern const Gen2DistributionPokemon g2_pcny_sonicBoomScyther;
extern const Gen2DistributionPokemon g2_pcny_rockThrowPinsir;
extern const Gen2DistributionPokemon g2_pcny_quickAttackTauros;
extern const Gen2DistributionPokemon g2_pcny_bubbleMagikarp;
extern const Gen2DistributionPokemon g2_pcny_reversalMagikarp;
extern const Gen2DistributionPokemon g2_pcny_biteLapras;
extern const Gen2DistributionPokemon g2_pcny_futureSightLapras;
extern const Gen2DistributionPokemon g2_pcny_growthEevee;
extern const Gen2DistributionPokemon g2_pcny_barrierPorygon;
extern const Gen2DistributionPokemon g2_pcny_rockThrowOmanyte;
extern const Gen2DistributionPokemon g2_pcny_rockThrowKabuto;
extern const Gen2DistributionPokemon g2_pcny_rockThrowAerodactyl;
extern const Gen2DistributionPokemon g2_pcny_lovelyKissSnorlax;
extern const Gen2DistributionPokemon g2_pcny_splashSnorlax;
extern const Gen2DistributionPokemon g2_pcny_sweetKissSnorlax;
extern const Gen2DistributionPokemon g2_pcny_shinyArticuno;
extern const Gen2DistributionPokemon g2_pcny_shinyZapdos;
extern const Gen2DistributionPokemon g2_pcny_shinyMoltres;
extern const Gen2DistributionPokemon g2_pcny_hydroPumpDratini;
extern const Gen2DistributionPokemon g2_pcny_shinyMewtwo;
extern const Gen2DistributionPokemon g2_pcny_shinyMew;
extern const Gen2DistributionPokemon g2_pcny_petalDanceChikorita;
extern const Gen2DistributionPokemon g2_pcny_shinyMeganium;
extern const Gen2DistributionPokemon g2_pcny_doubleEdgeCyndaquil;
extern const Gen2DistributionPokemon g2_pcny_shinyTyphlosion;
extern const Gen2DistributionPokemon g2_pcny_submissionTotodile;
extern const Gen2DistributionPokemon g2_pcny_shinyFeraligatr;
extern const Gen2DistributionPokemon g2_pcny_dizzyPunchSentret;
extern const Gen2DistributionPokemon g2_pcny_nightShadeHoothoot;
extern const Gen2DistributionPokemon g2_pcny_barrierLedyba;
extern const Gen2DistributionPokemon g2_pcny_growthSpinarak;
extern const Gen2DistributionPokemon g2_pcny_lightScreenChinchou;
extern const Gen2DistributionPokemon g2_pcny_dizzyPunchPichu;
extern const Gen2DistributionPokemon g2_pcny_petalDancePichu;
extern const Gen2DistributionPokemon g2_pcny_scaryFacePichu;
extern const Gen2DistributionPokemon g2_pcny_singPichu;
extern const Gen2DistributionPokemon g2_pcny_petalDanceCleffa;
extern const Gen2DistributionPokemon g2_pcny_scaryFaceCleffa;
extern const Gen2DistributionPokemon g2_pcny_swiftCleffa;
extern const Gen2DistributionPokemon g2_pcny_mimicIgglybuff;
extern const Gen2DistributionPokemon g2_pcny_petalDanceIgglybuff;
extern const Gen2DistributionPokemon g2_pcny_scaryFaceIgglybuff;
extern const Gen2DistributionPokemon g2_pcny_safeguardNatu;
extern const Gen2DistributionPokemon g2_pcny_dizzyPunchMarill;
extern const Gen2DistributionPokemon g2_pcny_hydroPumpMarill;
extern const Gen2DistributionPokemon g2_pcny_scaryFaceMarill;
extern const Gen2DistributionPokemon g2_pcny_substituteSudowoodo;
extern const Gen2DistributionPokemon g2_pcny_agilityHoppip;
extern const Gen2DistributionPokemon g2_pcny_mimicAipom;
extern const Gen2DistributionPokemon g2_pcny_splashSunkern;
extern const Gen2DistributionPokemon g2_pcny_steelWingYanma;
extern const Gen2DistributionPokemon g2_pcny_sweetKissYanma;
extern const Gen2DistributionPokemon g2_pcny_bellyDrumWooper;
extern const Gen2DistributionPokemon g2_pcny_scaryFaceWooper;
extern const Gen2DistributionPokemon g2_pcny_beatUpMurkrow;
extern const Gen2DistributionPokemon g2_pcny_hypnosisMisdreavus;
extern const Gen2DistributionPokemon g2_pcny_mimicWobbuffet;
extern const Gen2DistributionPokemon g2_pcny_substitutePineco;
extern const Gen2DistributionPokemon g2_pcny_furyAttackDunsparce;
extern const Gen2DistributionPokemon g2_pcny_hornDrillDunsparce;
extern const Gen2DistributionPokemon g2_pcny_lovelyKissSnubbull;
extern const Gen2DistributionPokemon g2_pcny_doubleEdgeQwilfish;
extern const Gen2DistributionPokemon g2_pcny_seismicTossHeracross;
extern const Gen2DistributionPokemon g2_pcny_moonlightSneasel;
extern const Gen2DistributionPokemon g2_pcny_sweetScentTeddiursa;
extern const Gen2DistributionPokemon g2_pcny_whirlwindSwinub;
extern const Gen2DistributionPokemon g2_pcny_amnesiaRemoraid;
extern const Gen2DistributionPokemon g2_pcny_mistRemoraid;
extern const Gen2DistributionPokemon g2_pcny_payDayDelibird;
extern const Gen2DistributionPokemon g2_pcny_spikesDelibird;
extern const Gen2DistributionPokemon g2_pcny_gustMantine;
extern const Gen2DistributionPokemon g2_pcny_furyCutterSkarmory;
extern const Gen2DistributionPokemon g2_pcny_absorbPhanpy;
extern const Gen2DistributionPokemon g2_pcny_safeguardStantler;
extern const Gen2DistributionPokemon g2_pcny_rageTyrogue;
extern const Gen2DistributionPokemon g2_pcny_metronomeSmoochum;
extern const Gen2DistributionPokemon g2_pcny_petalDanceSmoochum;
extern const Gen2DistributionPokemon g2_pcny_dizzyPunchElekid;
extern const Gen2DistributionPokemon g2_pcny_pursuitElekid;
extern const Gen2DistributionPokemon g2_pcny_faintAttackMagby;
extern const Gen2DistributionPokemon g2_pcny_megaKickMiltank;
extern const Gen2DistributionPokemon g2_pcny_shinyRaikou;
extern const Gen2DistributionPokemon g2_pcny_shinyEntei;
extern const Gen2DistributionPokemon g2_pcny_shinySuicune;
// The regular (non-shiny) suicune on https://bulbapedia.bulbagarden.net/wiki/List_of_PCNY_event_Pok%C3%A9mon_distributions_(Generation_II)#Suicune
// has too many question marks and not even any moves. So I'm ignoring that one.
//extern const Gen2DistributionPokemon g2_pcny_suicune;
extern const Gen2DistributionPokemon g2_pcny_rageLarvitar;
extern const Gen2DistributionPokemon g2_pcny_shinyLugia;
extern const Gen2DistributionPokemon g2_pcny_shinyHoOh;
extern const Gen2DistributionPokemon g2_pcny_Celebi;
/**
* @brief Gets a list of const Gen2DistributionPokemon pointers of the distribution pokemon that were available worldwide
* and stores it in the specified outList.
*
* outSize will be updated with the size of the list
*/
void gen2_getMainDistributionPokemonList(const Gen2DistributionPokemon**& outList, uint32_t& outSize);
/**
* @brief Gets a list of const Gen2DistributionPokemon pointers of the distribution pokemon that were available at
* the Pokémon Center New York (PCNY) and stores it in the specified outList.
*
* outSize will be updated with the size of the list
*/
void gen2_getPokemonCenterNewYorkDistributionPokemonList(const Gen2DistributionPokemon**& outList, uint32_t& outSize);
#endif

View File

@ -0,0 +1,189 @@
#ifndef _GEN2GAMEREADER_H
#define _GEN2GAMEREADER_H
#include "gen2/Gen2SpriteDecoder.h"
#include "gen2/Gen2PlayerPokemonStorage.h"
#include "gen2/Gen2DistributionPokemon.h"
class IRomReader;
class ISaveManager;
// Note: for GEN2 there is no difference between index and pokedex number: the indexes are in national dex order
class Gen2GameReader
{
public:
Gen2GameReader(IRomReader &romReader, ISaveManager &saveManager, Gen2GameType gameType);
~Gen2GameReader();
Gen2GameType getGameType() const;
/**
* @brief get the name of a pokémon based on an index number
* Note: you don't own the returned pointer. The data will get overwritten on the next call to this function,
* so make sure to strdup() it if you need to store it for later
*
* @return const char*
*/
const char *getPokemonName(uint8_t index) const;
/**
* @brief With this function, you can check if the index is valid and not referring to garbage data
*/
bool isValidIndex(uint8_t index) const;
/**
* @brief This function reads the pokemon stats at the given index
*
* @return whether or not this operation was successful
*/
bool readPokemonStatsForIndex(uint8_t index, Gen2PokeStats &outStats) const;
/**
* @brief This function reads the pointer to the front sprite of the pokémon at the given index
*/
bool readFrontSpritePointer(uint8_t index, uint8_t &outBankIndex, uint16_t &outPointer);
/**
* @brief Converts the sprite_dimensions field in Gen2PokeStats into values we can actually use
*/
void readSpriteDimensions(const Gen2PokeStats &poke, uint8_t &outWidthInTiles, uint8_t &outHeightInTiles);
/**
* @brief This function reads a color palette for the given pokemon
* @param index the index of the pokemon
* @param shiny whether or not we need to get the shiny color palette
* @param outColorPalette the output variable in which the color palette will be read. Please make sure this array has a length of 4 items
*/
bool readColorPaletteForPokemon(uint8_t index, bool shiny, uint16_t *outColorPalette);
/**
* @brief This function decodes a sprite at the given bank and pointer.
* Returns a pointer to a buffer containing the decoded gameboy sprite in gameboy sprite format.
* You need to look at the sprite dimension to see how much data there is. ()
*
* WARNING: this function returns a pointer to an internal buffer of the Gen1SpriteDecoder. 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 *decodeSprite(uint8_t bankIndex, uint16_t pointer);
/**
* @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.
*
* WARNING: When you're done adding pokemon, make sure to call finishSave() !!! Not doing so will corrupt the save file!
*
* @param isEgg adds the pokemon as an egg
* @param originalTrainerID optional parameter. If not specified (=null), the original trainer id string will be set to the players'
* @param nickname optional parameter. If not specified (= null), the pokemon species name will be used instead.
*
* @return
* 0xFF - Could not add pokemon (probably no space left)
* 0xFE - Added to players' party
* 0x0 - 0xB - Added to box at index <value>
*/
uint8_t addPokemon(Gen2TrainerPokemon &poke, bool isEgg, const char *originalTrainerID = 0, const char *nickname = 0);
/**
* @brief Adds a distribution 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
*
* WARNING: When you're done adding pokemon, make sure to call finishSave() !!! Not doing so will corrupt the save file!
*
* @param nickname optional parameter. If not specified (= null), the pokemon species name will be used instead.
*
* @return
* 0xFF - Could not add pokemon (probably no space left)
* 0xFE - Added to players' party
* 0x0 - 0xB - Added to box at index <value>
*/
uint8_t addDistributionPokemon(const Gen2DistributionPokemon& distributionPoke, const char *nickname = 0);
/**
* @brief Get the player ID from the save file
*/
uint16_t getTrainerID() const;
/**
* @brief Get the trainer name from the save file
* Note: the resulting const char* does not need to be free'd.
* However, it needs to be either used rightaway or strdup()'d, because the data will get overwritten on the next call to this function.
*/
const char *getTrainerName() const;
/**
* @brief Get the rival name from the save file
* Note: the resulting const char* does not need to be free'd.
* However, it needs to be either used rightaway or strdup()'d, because the data will get overwritten on the next call to this function.
*/
const char *getRivalName() const;
/**
* @brief Returns the 0-based index of the currently selected PC box for pokemon
*/
uint8_t getCurrentBoxIndex();
/**
* @brief Returns a Gen2Party instance, which can be used to retrieve the information about the pokemon currently in the trainers' party
*/
Gen2Party getParty();
/**
* @brief Returns a Gen2Box instance, which can be used to retrieve information about the pokemon currently in the Trainers' PC box at the specified index
*/
Gen2Box getBox(uint8_t boxIndex);
/**
* Returns a Gen2ItemList instance, which can be used to retrieve the items currently in the players' specified inventory type
*/
Gen2ItemList getItemList(Gen2ItemListType type);
/**
* @brief This function returns whether the pokemon with the given pokedexNumber has been SEEN/OWNED.
*/
bool getPokedexFlag(PokedexFlag dexFlag, uint8_t pokedexNumber);
/**
* @brief This function sets the pokedex flag for the given pokedexNumber
*/
void setPokedexFlag(PokedexFlag dexFlag, uint8_t pokedexNumber);
/**
* @brief This function returns the counter value of the given dex flag
*/
uint8_t getPokedexCounter(PokedexFlag dexFlag);
/**
* @brief The save data is stored twice in gen 2. There's a main and a backup region.
* This function returns whether the main regions' checksum is valid.
*/
bool isMainChecksumValid();
/**
* @brief The save data is stored twice in gen 2. There's a main and a backup region.
* This function returns whether the backup regions' checksum is valid.
*/
bool isBackupChecksumValid();
/**
* @brief This function finishes the save file. Make sure to call this at the end after you're done doing changes
* This function will:
* - update the current pc box section of the save
* - update the main checksum
* - copy the main save to the backup save
*/
void finishSave();
/**
* @brief Checks whether we're currently dealing with Pokémon Crystal
*/
bool isGameCrystal() const;
protected:
private:
IRomReader &romReader_;
ISaveManager &saveManager_;
Gen2SpriteDecoder spriteDecoder_;
Gen2GameType gameType_;
};
#endif

View File

@ -0,0 +1,110 @@
#ifndef _GEN2PLAYERPOKEMONSTORAGE_H
#define _GEN2PLAYERPOKEMONSTORAGE_H
#include "Gen2Common.h"
class ISaveManager;
class Gen2GameReader;
// the numbers are based on the PC_BOX_LIST_CAPACITY and PC_BOX_LIST_PKMN_ENTRY_SIZE in combination with the formula you can find here:
// https://bulbapedia.bulbagarden.net/wiki/Save_data_structure_(Generation_II)#Pok%C3%A9mon_lists
const int GEN2_PC_BOX_SIZE_IN_BYTES = 20 * (32 + 23) + 2;
/**
* @brief The storage of the trainer party and pc boxes is very similar in Gen 2,
* aside from the listCapacity and the fields to read for a pokemon entry.
*
* Therefore this class abstracts them both
*
*/
class Gen2PokemonList
{
public:
Gen2PokemonList(Gen2GameReader& gameReader, ISaveManager& saveManager, uint8_t listCapacity, uint8_t entrySize);
virtual ~Gen2PokemonList();
virtual uint32_t getSaveOffset() = 0;
/**
* @brief Seeks to the beginning of the PokemonList with saveManager_ and returns the number of entries in the list
*/
uint8_t getNumberOfPokemon();
uint8_t getMaxNumberOfPokemon() const;
bool setNumberOfPokemon(uint8_t count);
/**
* @brief Seeks to the given index and returns the species index at that position
* If the last call on the saveManager instance was done with PokemonList::getCount(), you can avoid the seek by using getNextSpecies() instead.
*/
uint8_t getSpeciesAtIndex(uint8_t index);
/**
* @brief Modifies the species index at the given list index
*/
bool setSpeciesAtIndex(uint8_t index, uint8_t speciesIndex);
/**
* @brief Get the next species index in the list. This should only be used if the last call on saveManager was done with
* PokemonList::getCount(), PokemonList::getSpeciesAtIndex() or getNextSpecies()
*/
uint8_t getNextSpecies();
/**
* @brief Seeks to the data entry of the pokemon at the given index and reads its contents into Gen2TrainerPokemon
* if accessing subsequent entries, you can avoid the seek by using getNextPokemon() if the last call on the saveManager_
* instance was PokemonList::getPokemonAtIndex(), PokemonList::getNextPokemon(),or the terminator value of the species list obtained through
* getNextSpecies()
*/
bool getPokemon(uint8_t index, Gen2TrainerPokemon& outPoke);
/**
* @brief Checks whether the pokemon at the given index is an egg
*/
bool isEgg(uint8_t index);
/**
* @brief Gets the next pokemon entry in the list
*/
bool getNextPokemon(Gen2TrainerPokemon& outPoke);
bool setPokemon(uint8_t index, const Gen2TrainerPokemon& poke, bool isEgg);
bool add(const Gen2TrainerPokemon& poke, bool isEgg, const char* originalTrainerID, const char* nickname = nullptr);
const char* getPokemonNickname(uint8_t index);
bool setPokemonNickname(uint8_t, const char* nickname = nullptr);
const char* getOriginalTrainerOfPokemon(uint8_t index);
bool setOriginalTrainerOfPokemon(uint8_t index, const char* originalTrainerID);
protected:
Gen2GameReader& gameReader_;
ISaveManager& saveManager_;
private:
const uint8_t listCapacity_;
const uint8_t entrySize_;
};
class Gen2Party : public Gen2PokemonList
{
public:
Gen2Party(Gen2GameReader& gameReader, ISaveManager& saveManager);
virtual ~Gen2Party();
uint32_t getSaveOffset() override;
protected:
private:
};
class Gen2Box : public Gen2PokemonList
{
public:
Gen2Box(Gen2GameReader& gameReader, ISaveManager& saveManager, uint8_t boxIndex);
virtual ~Gen2Box();
uint32_t getSaveOffset() override;
protected:
private:
const uint8_t boxIndex_;
};
#endif

View File

@ -0,0 +1,60 @@
#ifndef _GEN2SPRITEDECODER_H
#define _GEN2SPRITEDECODER_H
#include <cstdint>
#define SPRITE_BUFFER_WIDTH_IN_PIXELS 56
#define SPRITE_BUFFER_HEIGHT_IN_PIXELS 56
#define GEN2_SPRITE_BUFFER_SIZE_IN_BYTES ((SPRITE_BUFFER_WIDTH_IN_PIXELS / 4) * SPRITE_BUFFER_HEIGHT_IN_PIXELS)
// Pokemon crystal has animated sprites. So a front sprite could consist of several frames.
// Some of the commands are therefore also referencing data that could be outside the bounds of a single frame.
// we need to ensure our buffer is large enough to cope with this.
// Essentially, the buffer should be large enough to contain the maximum number of frames used in pokémon crystal for a single sprite
// the number assigned to this define == the max number of frames you're expecting.
#define POKEMON_CRYSTAL_SAFETY_FACTOR 5
class IRomReader;
class Gen2SpriteDecoder
{
public:
Gen2SpriteDecoder(IRomReader& romReader);
~Gen2SpriteDecoder();
/**
* @brief Decodes the sprite that starts at the current position inside the romReader specified in the constructor.
*
* @return uint8_t* returns a pointer to an internal buffer containing the decoded sprite in gameboy format. If you need to convert to RGB, please use SpriteDecoder
* You don't own the returned buffer. In fact, the content will get overwritten on the next call to the decode() function. If you need to keep the data around, consider
* copying it to a separate buffer.
*/
uint8_t* decode();
/**
* @brief Decodes the sprite that starts in the given bank at the given pointer address. The pointer address should be a value between 0x4000 - 0x8000 because it refers
* to the address of bank 1 after it has been mapped to the address range of the gameboy.
*
* @return uint8_t* returns a pointer to an internal buffer containing the decoded sprite in gameboy format. If you need to convert to RGB, please use SpriteDecoder
* You don't own the returned buffer. In fact, the content will get overwritten on the next call to the decode() function. If you need to keep the data around, consider
* copying it to a separate buffer.
*/
uint8_t* decode(uint8_t bankIndex, uint16_t pointer);
protected:
private:
void decodeCommand(uint8_t command, uint16_t count);
/**
* @brief This function processes one of the 3 commands
* that look back into the data that was already filled in our buffer
* and reuses it
*
* There's some common code for these commands, that's why this function exists
*/
void processReuseCommand(uint8_t command, uint16_t count);
IRomReader& romReader_;
uint8_t buffer_[GEN2_SPRITE_BUFFER_SIZE_IN_BYTES * POKEMON_CRYSTAL_SAFETY_FACTOR];
uint8_t* cur_;
};
#endif

42
include/utils.h Normal file
View File

@ -0,0 +1,42 @@
#ifndef UTILS_H
#define UTILS_H
#include <cstdint>
struct TextCodePair;
/**
* @brief Searches the specified buffer (haystack) for a sequence of bytes (needle)
* @return uint8_t* NULL if not found, non-null if found
*/
const uint8_t* memSearch(const uint8_t* haystack, uint32_t hayStackLength, const uint8_t* needle, uint32_t needleLength);
/**
* @brief This function decodes a text (This could be a name or something else) found in the rom based on the textCodes list you pass in.
* @return the number of characters copied to the output buffer
*/
uint16_t decodeText(struct TextCodePair* textCodes, uint16_t numTextCodes, const uint8_t* inputBuffer, uint16_t inputBufferLength, char* outputBuffer, uint16_t outputBufferLength);
/**
* @brief The opposite of decodeText()
*/
uint16_t encodeText(struct TextCodePair* textCodes, uint16_t numTextCodes, const char* inputBuffer, uint16_t inputBufferLength, uint8_t* outputBuffer, uint16_t outputBufferLength, uint8_t terminator);
bool isCurrentCPULittleEndian();
uint16_t byteSwapUint16(uint16_t val);
uint32_t byteSwapUint32(uint32_t val);
/**
* Function to write an RGB24 image to a PNG file
*/
bool write_png(const char *filename, unsigned char *rgb_data, int width, int height);
/**
* @brief This function reads an entire file into a buffer.
* The caller needs to free() it after using it.
*
* @return a pointer to the allocated buffer. If something failed, NULL will be returned.
*/
uint8_t* readFileIntoBuffer(const char* pathToFile, uint32_t& outFileSize);
#endif

249
src/RomReader.cpp Normal file
View File

@ -0,0 +1,249 @@
#include "RomReader.h"
#include "utils.h"
#include <cstring>
static const uint8_t bitIndexMaskLookUpTable[] = {
0xFF,
0x40,
0x20,
0x10,
0x8,
0x4,
0x2,
0x1
};
static uint8_t readBit(IRomReader& romReader, uint8_t& bitIndex)
{
const uint8_t result = (romReader.peek() & bitIndexMaskLookUpTable[bitIndex]) >> (7 - bitIndex);
if(bitIndex == 7)
{
romReader.advance();
bitIndex = 0;
}
else
{
++bitIndex;
}
return result;
}
IRomReader::~IRomReader()
{
}
BaseRomReader::BaseRomReader()
: cpuLittleEndian_(isCurrentCPULittleEndian())
{
}
BaseRomReader::~BaseRomReader()
{
}
bool BaseRomReader::readUint16(uint16_t& outByte, Endianness fieldEndianness)
{
if(!read((uint8_t*)&outByte, 2))
{
return false;
}
if(fieldEndianness == Endianness::LITTLE && !cpuLittleEndian_)
{
outByte = byteSwapUint16(outByte);
}
else if(fieldEndianness == Endianness::BIG && cpuLittleEndian_)
{
outByte = byteSwapUint16(outByte);
}
return true;
}
bool BaseRomReader::readUint32(uint32_t& outByte, Endianness fieldEndianness)
{
if(!read((uint8_t*)&outByte, 4))
{
return false;
}
if(fieldEndianness == Endianness::LITTLE && !cpuLittleEndian_)
{
outByte = byteSwapUint32(outByte);
}
else if(fieldEndianness == Endianness::BIG && cpuLittleEndian_)
{
outByte = byteSwapUint32(outByte);
}
return true;
}
bool BaseRomReader::seekToBank(uint8_t bankIndex)
{
// 0x4000 represents 16 KB. Each gameboy rom bank has a size of 16 KB
return seek(((uint32_t)bankIndex) * 0x4000);
}
bool BaseRomReader::seekToRomPointer(uint16_t pointer, uint8_t bankIndex)
{
if(bankIndex == 0xFF)
{
bankIndex = getCurrentBankIndex();
}
if(!seekToBank(bankIndex))
{
return false;
}
// This needs a bit of background:
// The gameboy can address 32 KB of rom data at one time.
// To be able to support a bigger rom, a bank switching mechanism exists in most cartridges.
// a ROM is divided into blocks (=banks) of 16 KB.
// The first 16 KB of gameboy address space (0x0000 - 0x4000) and will ALWAYS be mapped to bank 0 of the ROM data on the cartridge.
// It is not possible to switch this to a different bank
// The next 16 KB is switchable on command and is mapped from 0x4000 - 0x8000
// When we read pointers from the ROM, it can be either 2 bytes (little endian) or 3 bytes. In the case of 3 bytes, the additional 1 byte refers to the bank index.
// In any case, the 2 bytes refer to an absolute pointer in mapped gameboy memory when the correct bank has been mapped/mounted.
// Since the first 16 KB of the address space in that scenario ALWAYS refers to bank 0, we need to subtract it from the pointer (0x4000) in order to get an offset relative to the start of the bank it is referring to.
// source: https://www.smogon.com/smog/issue27/glitch
pointer -= 0x4000;
return advance(pointer);
}
uint32_t BaseRomReader::readUntil(uint8_t* outBuffer, uint8_t terminator, uint32_t maxRead)
{
uint8_t* cur = outBuffer;
while(static_cast<uint32_t>(cur - outBuffer) < maxRead && read(cur, 1))
{
if((*cur) == terminator)
{
break;
}
++cur;
}
return (cur - outBuffer);
}
BufferBasedRomReader::BufferBasedRomReader(const uint8_t* buffer, uint32_t bufferSize)
: buffer_(buffer)
, end_(buffer + bufferSize)
, cur_(buffer_)
{
}
BufferBasedRomReader::~BufferBasedRomReader()
{
}
bool BufferBasedRomReader::readByte(uint8_t& outByte)
{
return read(&outByte, 1);
}
bool BufferBasedRomReader::read(uint8_t* outBuffer, uint32_t bytesToRead)
{
const uint8_t* newPos = cur_ + bytesToRead;
if(newPos <= end_)
{
memcpy(outBuffer, cur_, bytesToRead);
cur_ = newPos;
return true;
}
return false;
}
uint8_t BufferBasedRomReader::peek()
{
return (*cur_);
}
bool BufferBasedRomReader::advance(uint32_t numBytes)
{
return seek((cur_ - buffer_) + numBytes);
}
bool BufferBasedRomReader::seek(uint32_t absoluteOffset)
{
const uint8_t* newPos = buffer_ + absoluteOffset;
if(newPos < end_)
{
cur_ = newPos;
return true;
}
return false;
}
bool BufferBasedRomReader::searchFor(const uint8_t* needle, uint32_t needleLength)
{
const uint8_t* ret = memSearch(cur_, (end_ - cur_), needle, needleLength);
if(ret)
{
cur_ = ret;
return true;
}
return false;
}
uint8_t BufferBasedRomReader::getCurrentBankIndex() const
{
// every bank is 16 KB (=0x4000)
return (uint8_t)((cur_ - buffer_) % 0x4000);
}
BitReader::BitReader(IRomReader& romReader)
: romReader_(romReader)
, bitIndex_(0)
{
}
uint8_t BitReader::read(uint8_t numBits)
{
uint8_t result = 0;
uint8_t i;
for(i=0; i < numBits; ++i)
{
result <<= 1;
result |= readBit(romReader_, bitIndex_);
}
return result;
}
bool BitReader::seek(uint32_t byteOffset, uint8_t bitOffset)
{
if(!romReader_.seek(byteOffset))
{
return false;
}
bitIndex_ = bitOffset;
return true;
}
bool readGameboyCartridgeHeader(IRomReader& romReader, GameboyCartridgeHeader& cartridgeHeader)
{
memset(&cartridgeHeader, 0, sizeof(struct GameboyCartridgeHeader));
romReader.seek(0x100);
romReader.read(cartridgeHeader.entry, 4);
romReader.seek(0x134);
romReader.read((uint8_t*)cartridgeHeader.title, 16);
romReader.seek(0x13F);
romReader.read((uint8_t*)cartridgeHeader.manufacturer_code, 4);
romReader.seek(0x143);
romReader.readByte(cartridgeHeader.CGB_flag);
romReader.read((uint8_t*)cartridgeHeader.new_licensee_code, 2);
romReader.readByte(cartridgeHeader.SGB_flag);
romReader.readByte(cartridgeHeader.cartridge_type);
romReader.readByte(cartridgeHeader.rom_size);
romReader.readByte(cartridgeHeader.ram_size);
romReader.readByte(cartridgeHeader.destination_code);
romReader.readByte(cartridgeHeader.old_licensee_code);
romReader.readByte(cartridgeHeader.mask_rom_version_number);
romReader.readByte(cartridgeHeader.header_checksum);
romReader.read(cartridgeHeader.global_checksum, 2);
return true;
}

255
src/SaveManager.cpp Normal file
View File

@ -0,0 +1,255 @@
#include "SaveManager.h"
#include "utils.h"
#include <cstring>
ISaveManager::~ISaveManager()
{
}
BaseSaveManager::BaseSaveManager()
: cpuLittleEndian_(isCurrentCPULittleEndian())
{
}
BaseSaveManager::~BaseSaveManager()
{
}
bool BaseSaveManager::readUint16(uint16_t& outBytes, Endianness fieldEndianness)
{
if(!read((uint8_t*)&outBytes, 2))
{
return false;
}
if(fieldEndianness == Endianness::LITTLE && !cpuLittleEndian_)
{
outBytes = byteSwapUint16(outBytes);
}
else if(fieldEndianness == Endianness::BIG && cpuLittleEndian_)
{
outBytes = byteSwapUint16(outBytes);
}
return true;
}
void BaseSaveManager::writeUint16(uint16_t bytes, Endianness fieldEndianness)
{
// when reading the data before, we might have done a byte swap. To undo it while writing, we just need to repeat it.
if(fieldEndianness == Endianness::LITTLE && !cpuLittleEndian_)
{
bytes = byteSwapUint16(bytes);
}
else if(fieldEndianness == Endianness::BIG && cpuLittleEndian_)
{
bytes = byteSwapUint16(bytes);
}
write((uint8_t*)&bytes, 2);
}
bool BaseSaveManager::readUint24(uint32_t& outByte, Endianness fieldEndianness)
{
uint8_t bytes[3];
if(!read(bytes, 3))
{
return false;
}
if(fieldEndianness == Endianness::LITTLE)
{
outByte = (((uint32_t)bytes[2]) << 16) | (((uint32_t)bytes[1]) << 8) | bytes[0];
}
else
{
outByte = (((uint32_t)bytes[0]) << 16) | (((uint32_t)bytes[1]) << 8) | bytes[2];
}
return true;
}
void BaseSaveManager::writeUint24(uint32_t bytes, Endianness fieldEndianness)
{
uint8_t bytesToWrite[3];
if(fieldEndianness == Endianness::LITTLE)
{
bytesToWrite[0] = (uint8_t)(bytes & 0xFF);
bytesToWrite[1] = (uint8_t)((bytes & 0xFF00) >> 8);
bytesToWrite[2] = (uint8_t)((bytes & 0xFF0000) >> 16);
}
else
{
bytesToWrite[0] = (uint8_t)((bytes & 0xFF0000) >> 16);
bytesToWrite[1] = (uint8_t)((bytes & 0xFF00) >> 8);
bytesToWrite[2] = (uint8_t)(bytes & 0xFF);
}
write(bytesToWrite, sizeof(bytesToWrite));
}
bool BaseSaveManager::readUint32(uint32_t& outByte, Endianness fieldEndianness)
{
if(!read((uint8_t*)&outByte, 4))
{
return false;
}
if(fieldEndianness == Endianness::LITTLE && !cpuLittleEndian_)
{
outByte = byteSwapUint32(outByte);
}
else if(fieldEndianness == Endianness::BIG && cpuLittleEndian_)
{
outByte = byteSwapUint32(outByte);
}
return true;
}
bool BaseSaveManager::seekToBank(uint8_t bankIndex)
{
// 0x2000 represents 8KB. Each gameboy sram bank has a size of 8KB.
return seek(((uint32_t)bankIndex) * 0x2000);
}
bool BaseSaveManager::seekToBankOffset(uint8_t bankIndex, uint16_t offset)
{
if(!seekToBank(bankIndex))
{
return false;
}
return advance(offset);
}
uint32_t BaseSaveManager::readUntil(uint8_t* outBuffer, uint8_t terminator, uint32_t maxRead)
{
uint8_t* cur = outBuffer;
while(static_cast<uint32_t>(cur - outBuffer) < maxRead && read(cur, 1))
{
if((*cur) == terminator)
{
break;
}
++cur;
}
return (cur - outBuffer);
}
void BaseSaveManager::copyRegion(uint32_t absoluteSrcOffset, uint32_t absoluteDstOffsetStart, uint32_t numBytes)
{
uint8_t buffer[128];
uint32_t srcOffset = absoluteSrcOffset;
// include the end byte of the range
uint32_t bytesRemaining = numBytes;
uint8_t bytesToRead;
while(bytesRemaining > 0)
{
if(bytesRemaining > sizeof(buffer))
{
bytesToRead = sizeof(buffer);
}
else
{
bytesToRead = bytesRemaining;
}
seek(srcOffset);
read(buffer, bytesToRead);
seek(absoluteDstOffsetStart + numBytes - bytesRemaining);
write(buffer, bytesToRead);
srcOffset += bytesToRead;
bytesRemaining -= bytesToRead;
}
}
BufferBasedSaveManager::BufferBasedSaveManager(uint8_t* buffer, uint32_t bufferSize)
: buffer_(buffer)
, end_(buffer + bufferSize)
, cur_(buffer)
{
}
BufferBasedSaveManager::~BufferBasedSaveManager()
{
}
bool BufferBasedSaveManager::readByte(uint8_t& outByte)
{
return read(&outByte, 1);
}
void BufferBasedSaveManager::writeByte(uint8_t byte)
{
write(&byte, 1);
}
bool BufferBasedSaveManager::read(uint8_t* outBuffer, uint32_t bytesToRead)
{
uint8_t* newPos = cur_ + bytesToRead;
if(newPos < end_)
{
memcpy(outBuffer, cur_, bytesToRead);
cur_ = newPos;
return true;
}
return false;
}
void BufferBasedSaveManager::write(const uint8_t* buffer, uint32_t bytesToWrite)
{
uint8_t* newPos = cur_ + bytesToWrite;
if(newPos < end_)
{
memcpy(cur_, buffer, bytesToWrite);
cur_ = newPos;
}
}
uint8_t BufferBasedSaveManager::peek()
{
return (*cur_);
}
bool BufferBasedSaveManager::advance(uint32_t numBytes)
{
uint8_t* newPos = cur_ + numBytes;
if(newPos < end_)
{
cur_ = newPos;
return true;
}
return false;
}
bool BufferBasedSaveManager::rewind(uint32_t numBytes)
{
uint8_t* newPos = cur_ - numBytes;
if(newPos >= buffer_)
{
cur_ = newPos;
return true;
}
return false;
}
bool BufferBasedSaveManager::seek(uint32_t absoluteOffset)
{
uint8_t* newPos = buffer_ + absoluteOffset;
if(newPos < end_)
{
cur_ = newPos;
return true;
}
return false;
}
uint8_t BufferBasedSaveManager::getCurrentBankIndex() const
{
// 0x2000 represents the bank size of 8 KB
return (uint8_t)((cur_ - buffer_) / 0x2000);
}

70
src/SpriteRenderer.cpp Normal file
View File

@ -0,0 +1,70 @@
#include "SpriteRenderer.h"
#include "common.h"
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::drawRGB(const uint8_t* spriteBuffer, uint16_t palette[4], uint8_t numHTiles, uint8_t numVTiles)
{
uint8_t byte0;
uint8_t byte1;
uint16_t x;
uint16_t y;
// convert -size 56x56 -depth 8 rgb:sprite.rgb output.png
const uint8_t* rgbPalette = convertGBColorPaletteToRGB24(palette);
// 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;
uint8_t r, g, b;
// every tile is 8x8
const uint16_t spriteBufferHeightInPixels = numVTiles * 8;
const uint16_t spriteBufferWidthInPixels = numHTiles * 8;
const uint16_t spriteBufferSize = spriteBufferWidthInPixels * spriteBufferHeightInPixels / 8;
while(i < spriteBufferSize)
{
byte0 = spriteBuffer[i * 2];
byte1 = spriteBuffer[i * 2 + 1];
// printf("0x%hhu 0x%hhu\n", byte0, byte1);
x = (i / (spriteBufferHeightInPixels)) * 8;
y = (i % (spriteBufferHeightInPixels));
for(uint8_t bitIndex = 0; bitIndex < 8; ++bitIndex)
{
offset = (y * spriteBufferWidthInPixels + x + bitIndex) * 3;
bit0 = (byte0 >> (7 - bitIndex)) & 0x1;
bit1 = (byte1 >> (7 - bitIndex)) & 0x1;
paletteIndex = (bit1 << 1) | bit0;
r = rgbPalette[paletteIndex * 3 + 0];
g = rgbPalette[paletteIndex * 3 + 1];
b = rgbPalette[paletteIndex * 3 + 2];
rgbBuffer_[offset] = r;
rgbBuffer_[offset + 1] = g;
rgbBuffer_[offset + 2] = b;
}
++i;
}
return rgbBuffer_;
}

582
src/common.cpp Normal file
View File

@ -0,0 +1,582 @@
#include "common.h"
#include <cmath>
#include <cstdio>
uint16_t monochromeGBColorPalette[4] = {
0x7FFF,
0x56B6,
0x2D6B,
0x421
};
uint32_t mediumFastExpTable[] = {
0,
8,
27,
64,
125,
216,
343,
512,
729,
1000,
1331,
1728,
2197,
2744,
3375,
4096,
4913,
5832,
6859,
8000,
9261,
10648,
12167,
13824,
15625,
17576,
19683,
21952,
24389,
27000,
29791,
32768,
35937,
39304,
42875,
46656,
50653,
54872,
59319,
64000,
68921,
74088,
79507,
85184,
91125,
97336,
103823,
110592,
117649,
125000,
132651,
140608,
148877,
157464,
166375,
175616,
185193,
195112,
205379,
216000,
226981,
238328,
250047,
262144,
274625,
287496,
300763,
314432,
328509,
343000,
357911,
373248,
389017,
405224,
421875,
438976,
456533,
474552,
493039,
512000,
531441,
551368,
571787,
592704,
614125,
636056,
658503,
681472,
704969,
729000,
753571,
778688,
804357,
830584,
857375,
884736,
912673,
941192,
970299,
1000000,
};
uint32_t mediumSlowExpTable[] = {
0,
9,
57,
96,
135,
179,
236,
314,
419,
560,
742,
973,
1261,
1612,
2035,
2535,
3120,
3798,
4575,
5460,
6458,
7577,
8825,
10208,
11735,
13411,
15244,
17242,
19411,
21760,
24294,
27021,
29949,
33084,
36435,
40007,
43808,
47846,
52127,
56660,
61450,
66505,
71833,
77440,
83335,
89523,
96012,
102810,
109923,
117360,
125126,
133229,
141677,
150476,
159635,
169159,
179056,
189334,
199999,
211060,
222522,
234393,
246681,
259392,
272535,
286115,
300140,
314618,
329555,
344960,
360838,
377197,
394045,
411388,
429235,
447591,
466464,
485862,
505791,
526260,
547274,
568841,
590969,
613664,
636935,
660787,
685228,
710266,
735907,
762160,
789030,
816525,
844653,
873420,
902835,
932903,
963632,
995030,
1027103,
1059860,
};
uint32_t fastExpTable[] = {
0,
6,
21,
51,
100,
172,
274,
409,
583,
800,
1064,
1382,
1757,
2195,
2700,
3276,
3930,
4665,
5487,
6400,
7408,
8518,
9733,
11059,
12500,
14060,
15746,
17561,
19511,
21600,
23832,
26214,
28749,
31443,
34300,
37324,
40522,
43897,
47455,
51200,
55136,
59270,
63605,
68147,
72900,
77868,
83058,
88473,
94119,
100000,
106120,
112486,
119101,
125971,
133100,
140492,
148154,
156089,
164303,
172800,
181584,
190662,
200037,
209715,
219700,
229996,
240610,
251545,
262807,
274400,
286328,
298598,
311213,
324179,
337500,
351180,
365226,
379641,
394431,
409600,
425152,
441094,
457429,
474163,
491300,
508844,
526802,
545177,
563975,
583200,
602856,
622950,
643485,
664467,
685900,
707788,
730138,
752953,
776239,
800000,
};
uint32_t slowExpTable[] = {
0,
10,
33,
80,
156,
270,
428,
640,
911,
1250,
1663,
2160,
2746,
3430,
4218,
5120,
6141,
7290,
8573,
10000,
11576,
13310,
15208,
17280,
19531,
21970,
24603,
27440,
30486,
33750,
37238,
40960,
44921,
49130,
53593,
58320,
63316,
68590,
74148,
80000,
86151,
92610,
99383,
106480,
113906,
121670,
129778,
138240,
147061,
156250,
165813,
175760,
186096,
196830,
207968,
219520,
231491,
243890,
256723,
270000,
283726,
297910,
312558,
327680,
343281,
359370,
375953,
393040,
410636,
428750,
447388,
466560,
486271,
506530,
527343,
548720,
570666,
593190,
616298,
640000,
664301,
689210,
714733,
740880,
767656,
795070,
823128,
851840,
881211,
911250,
941963,
973360,
1005446,
1038230,
1071718,
1105920,
1140841,
1176490,
1212873,
1250000,
};
uint8_t* convertGBColorPaletteToRGB24(uint16_t palette[4])
{
static uint8_t result[12];
uint16_t work;
uint8_t *cur = result;
for(uint8_t i=0; i < 4; ++i)
{
// format is 15 bit BGR (https://www.huderlem.com/demos/gameboypalette.html)
const uint8_t b5 = (palette[i] >> 10) & 0x1F;
const uint8_t g5 = (palette[i] >> 5) & 0x1F;
const uint8_t r5 = palette[i] & 0x1F;
work = (static_cast<uint32_t>(r5) * 255) / 31;
(*cur) = (uint8_t)work;
++cur;
work = (static_cast<uint32_t>(g5) * 255) / 31;
(*cur) = (uint8_t)work;
++cur;
work = (static_cast<uint32_t>(b5) * 255) / 31;
(*cur) = (uint8_t)work;
++cur;
}
return result;
}
uint8_t getStatIV(PokeStat type, const uint8_t iv_data[2])
{
// https://bulbapedia.bulbagarden.net/wiki/Individual_values
switch(type)
{
case PokeStat::ATK:
return (iv_data[0] >> 4);
case PokeStat::DEF:
return (iv_data[0] & 0xF);
case PokeStat::SPEED:
return (iv_data[1] >> 4);
case PokeStat::SPECIAL:
case PokeStat::SPECIAL_ATK:
case PokeStat::SPECIAL_DEF:
return (iv_data[1] & 0xF);
case PokeStat::HP:
// HP is based on the least significant bit of the other 4 stats
return (((getStatIV(PokeStat::ATK, iv_data) & 0x1) << 3) |
((getStatIV(PokeStat::DEF, iv_data) & 0x1) << 2) |
((getStatIV(PokeStat::SPEED, iv_data) & 0x1) << 1) |
((getStatIV(PokeStat::SPECIAL, iv_data) & 0x1)));
default:
return 0;
}
}
// https://bulbapedia.bulbagarden.net/wiki/Stat
// Note that there may be a difference between the stats in the Gen1TrainerPokemon stats field and the ones calculated here
// This is because stats are being recalculated when withdrawing the pokemon from the pc.
// look into "The box trick"
// If you store your pokemon in an ingame pc and take it out again, you'll see that now the stats match with the ones calculated here.
uint16_t calculatePokeStat(PokeStat type, uint16_t baseVal, uint8_t stat_iv, uint16_t stat_exp, uint8_t level)
{
uint16_t result;
// Terminology: For Gen 1 & 2, IVs are also referred to as Determinant Values (DVs)
// first part: The fraction only
// this part is common for all PokeStats including HP
const uint16_t ivPart = (baseVal + stat_iv) * 2;
const uint16_t statExpPart = static_cast<uint16_t>(ceil(sqrt(stat_exp)) / 4);
result = ((ivPart + statExpPart) * level) / 100;
if(type == PokeStat::HP)
{
// now add the second part of the formula, which is specific for HP
result += level + 10;
}
else
{
// now add the second part of the formula specific to all stats that aren't HP.
result += 5;
}
return static_cast<uint16_t>(result);
}
uint32_t *getExpTableForGrowthRate(uint8_t growthRate)
{
uint32_t *expTable;
switch (growthRate)
{
case GRW_FAST:
expTable = fastExpTable;
break;
case GRW_MEDIUM_FAST:
expTable = mediumFastExpTable;
break;
case GRW_MEDIUM_SLOW:
expTable = mediumSlowExpTable;
break;
case GRW_SLOW:
expTable = slowExpTable;
break;
default:
expTable = 0;
break;
}
return expTable;
}
uint32_t getExpNeededForLevel(uint8_t level, uint8_t growthRate)
{
uint32_t *expTable;
if(level < 1 || level > 100)
{
return 0;
}
expTable = getExpTableForGrowthRate(growthRate);
return (expTable) ? expTable[level - 1] : 0;
}
uint8_t getLevelForExp(uint32_t exp, uint8_t growthRate)
{
const uint32_t *expTable = getExpTableForGrowthRate(growthRate);
if (!expTable)
{
return 1;
}
for (uint8_t i = 1; i < 100; ++i)
{
// to check which level we have, we need to find the first entry that is more than the exp we have
// when we find that, i would refer to the next level, not the current one.
// However, since we're doing an array index, i is zero-based, whereas the pkmn level is 1 -based.
// so it turns out that we can just return i here instead of subtracting by 1
if (exp < expTable[i])
{
return i;
}
}
return 100;
}
void printGameboyCartridgeHeader(const GameboyCartridgeHeader& h)
{
printf("Entry:\t%hhx %hhx %hhx %hhx\n", h.entry[0], h.entry[1], h.entry[2], h.entry[3]);
printf("Title:\t%s\n", h.title);
printf("Manufacturer code:\t%s\n", h.manufacturer_code);
printf("CGB flag:\t%hhx\n", h.CGB_flag);
printf("New Licensee code:\t%hhx %hhx\n", h.new_licensee_code[0], h.new_licensee_code[1]);
printf("SGB flag:\t%hhx\n", h.SGB_flag);
printf("Cartridge type:\t%hhx\n", h.cartridge_type);
printf("Rom size:\t%hhx\n", h.rom_size);
printf("Ram size:\t%hhx\n", h.ram_size);
printf("Destination code:\t%hhx\n", h.destination_code);
printf("Old Licensee code:\t%hhx\n", h.old_licensee_code);
printf("Mask Rom version:\t%hhx\n", h.mask_rom_version_number);
printf("Header checksum:\t%hhx\n", h.header_checksum);
printf("Global checksum: \t%hhx %hhx\n", h.global_checksum[0], h.global_checksum[1]);
}

177
src/gen1/Gen1Common.cpp Normal file
View File

@ -0,0 +1,177 @@
#include "gen1/Gen1Common.h"
#include "gen1/Gen1GameReader.h"
#include "utils.h"
#include "common.h"
#include <cstring>
#define POKEMON_BLUE_CARTRIDGE_TITLE "POKEMON BLUE"
#define POKEMON_RED_CARTRIDGE_TITLE "POKEMON RED"
#define POKEMON_YELLOW_CARTRIDGE_TITLE "POKEMON YELLOW"
static TextCodePair gen1TextCodes[] = {
{0x4F, " "},
{0x57, "#"},
{0x51, "*"},
{0x52, "A1"},
{0x53, "A2"},
{0x54, "POKé"},
{0x55, "+"},
{0x58, "$"},
{0x5D, "TRAINER"},
{0x75, ""},
{0x7F, " "},
{0x80, "A"},
{0x81, "B"},
{0x82, "C"},
{0x83, "D"},
{0x84, "E"},
{0x85, "F"},
{0x86, "G"},
{0x87, "H"},
{0x88, "I"},
{0x89, "J"},
{0x8A, "K"},
{0x8B, "L"},
{0x8C, "M"},
{0x8D, "N"},
{0x8E, "O"},
{0x8F, "P"},
{0x90, "Q"},
{0x91, "R"},
{0x92, "S"},
{0x93, "T"},
{0x94, "U"},
{0x95, "V"},
{0x96, "W"},
{0x97, "X"},
{0x98, "Y"},
{0x99, "Z"},
{0x9A, "("},
{0x9B, ")"},
{0x9C, ","},
{0x9D, ";"},
{0x9E, "["},
{0x9F, "]"},
{0xA0, "a"},
{0xA1, "b"},
{0xA2, "c"},
{0xA3, "d"},
{0xA4, "e"},
{0xA5, "f"},
{0xA6, "g"},
{0xA7, "h"},
{0xA8, "i"},
{0xA9, "j"},
{0xAA, "k"},
{0xAB, "l"},
{0xAC, "m"},
{0xAD, "n"},
{0xAE, "o"},
{0xAF, "p"},
{0xB0, "q"},
{0xB1, "r"},
{0xB2, "s"},
{0xB3, "t"},
{0xB4, "u"},
{0xB5, "v"},
{0xB6, "w"},
{0xB7, "x"},
{0xB8, "y"},
{0xB9, "z"},
{0xBA, "é"},
{0xBB, "\'d"},
{0xBC, "\'l"},
{0xBD, "\'s"},
{0xBE, "\'t"},
{0xBF, "\'v"},
{0xE0, "\'"},
{0xE1, "PK"},
{0xE2, "MN"},
{0xE3, "-"},
{0xE4, "\'r"},
{0xE5, "\'m"},
{0xE6, "?"},
{0xE7, "!"},
{0xE8, "."},
{0xED, ""},
{0xEE, ""},
{0xEF, ""},
{0xF0, "¥"},
{0xF1, "×"},
{0xF3, "/"},
{0xF4, ","},
{0xF5, ""},
{0xF6, "0"},
{0xF7, "1"},
{0xF8, "2"},
{0xF9, "3"},
{0xFA, "4"},
{0xFB, "5"},
{0xFC, "6"},
{0xFD, "7"},
{0xFE, "8"},
{0xFF, "9"}
};
Gen1GameType gen1_determineGameType(const GameboyCartridgeHeader& cartridgeHeader)
{
Gen1GameType result;
if (strncmp(cartridgeHeader.title, POKEMON_BLUE_CARTRIDGE_TITLE, sizeof(POKEMON_BLUE_CARTRIDGE_TITLE) - 1) == 0)
{
result = Gen1GameType::BLUE;
}
else if (strncmp(cartridgeHeader.title, POKEMON_RED_CARTRIDGE_TITLE, sizeof(POKEMON_RED_CARTRIDGE_TITLE) - 1) == 0)
{
result = Gen1GameType::RED;
}
else if (strncmp(cartridgeHeader.title, POKEMON_YELLOW_CARTRIDGE_TITLE, sizeof(POKEMON_YELLOW_CARTRIDGE_TITLE) - 1) == 0)
{
result = Gen1GameType::YELLOW;
}
else
{
result = Gen1GameType::INVALID;
}
return result;
}
void gen1_recalculatePokeStats(Gen1GameReader& reader, Gen1TrainerPokemon& poke)
{
Gen1PokeStats stats;
reader.readPokemonStatsForIndex(poke.poke_index, stats);
poke.max_hp = calculatePokeStat(PokeStat::HP, stats.base_hp, getStatIV(PokeStat::HP, poke.iv_data), poke.hp_effort_value, poke.level);
poke.atk = calculatePokeStat(PokeStat::ATK, stats.base_attack, getStatIV(PokeStat::ATK, poke.iv_data), poke.atk_effort_value, poke.level);
poke.def = calculatePokeStat(PokeStat::DEF, stats.base_defense, getStatIV(PokeStat::DEF, poke.iv_data), poke.def_effort_value, poke.level);
poke.speed = calculatePokeStat(PokeStat::SPEED, stats.base_speed, getStatIV(PokeStat::SPEED, poke.iv_data), poke.speed_effort_value, poke.level);
poke.special = calculatePokeStat(PokeStat::SPECIAL, stats.base_special, getStatIV(PokeStat::SPECIAL, poke.iv_data), poke.special_effort_value, poke.level);
}
uint16_t gen1_decodePokeText(const uint8_t* inputBuffer, uint16_t inputBufferLength, char* outputBuffer, uint16_t outputBufferLength)
{
const uint16_t numEntries = sizeof(gen1TextCodes) / sizeof(struct TextCodePair);
return decodeText(gen1TextCodes, numEntries, inputBuffer, inputBufferLength, outputBuffer, outputBufferLength);
}
uint16_t gen1_encodePokeText(const char* inputBuffer, uint16_t inputBufferLength, uint8_t* outputBuffer, uint16_t outputBufferLength, uint8_t terminator)
{
const uint16_t numEntries = sizeof(gen1TextCodes) / sizeof(struct TextCodePair);
return encodeText(gen1TextCodes, numEntries, inputBuffer, inputBufferLength, outputBuffer, outputBufferLength, terminator);
}
Gen1Checksum::Gen1Checksum()
: checksum_(0)
{
}
void Gen1Checksum::addByte(uint8_t byteVal)
{
checksum_ += byteVal;
}
uint8_t Gen1Checksum::get() const
{
return ~checksum_;
}

View File

@ -0,0 +1,371 @@
#include "gen1/Gen1DistributionPokemon.h"
#include "common.h"
static const Gen1TrainerPokemon commonMewDefinition = {
0x15,
1, // I don't want to bother with figuring out the max hp here, so I set current hp to 1
5,
0,
0x18,
0x18,
45, // based on Gen1PokeStats
0x1, // pound
0x0,
0x0,
0x0,
0,
getExpNeededForLevel(5, 3),
0,
0,
0,
0,
0,
{0xA1, 0xC5},
35,
0,
0,
0,
0,
0,
0,
0,
0
};
static const Gen1TrainerPokemon surfingPikachu = {
0x54,
1,
5,
0,
0x17,
0x17,
190,
84,
45,
57,
0,
0,
getExpNeededForLevel(5, 0),
0,
0,
0,
0,
0,
{0, 0},
30,
40,
15,
0,
0,
0,
0,
0,
0
};
const Gen1DistributionPokemon g1_nintendoUKPokemonFestivalMew2016 = {
"Nintendo UK Pokemon Festival Mew 2016",
"GF",
22796,
{0xFF, 0xFF},
false,
false,
commonMewDefinition
};
const Gen1DistributionPokemon g1_clubNintendoMexicoGiveawayMew = {
"Club Nintendo Mexico Giveaway Mew",
"ASH",
45515,
{0xA1, 0xC5},
false,
false,
commonMewDefinition
};
const Gen1DistributionPokemon g1_farMorBornMew = {
"Far, Mor & Born Mew",
"",
0,
{0xA1, 0xC5},
true,
true,
commonMewDefinition
};
const Gen1DistributionPokemon g1_christmasPresentMew = {
"Christmas Present Mew",
"",
0,
{0xA1, 0xC5},
true,
true,
commonMewDefinition
};
const Gen1DistributionPokemon g1_swedenMewOnTour = {
"Sweden Mew On Tour",
"",
0,
{0xA1, 0xC5},
true,
true,
commonMewDefinition
};
const Gen1DistributionPokemon g1_tennispalatsiMew = {
"Tennispalatsi Mew",
"FINLAND",
1400,
{0xA1, 0xC5},
false,
true,
commonMewDefinition
};
const Gen1DistributionPokemon g1_norwayPokemonTourMew = {
"Norway Pokémon Tour Mew",
"EUROPE",
0,
{0xA1, 0xC5},
false,
true,
commonMewDefinition
};
const Gen1DistributionPokemon g1_clubNintendoMew = {
"Club Nintendo Mew",
"EUROPE",
0,
{0xA1, 0xC5},
false,
true,
commonMewDefinition
};
const Gen1DistributionPokemon g1_pokemon2000ChampionshipMew = {
"Pokémon 2000 World Championship Mew",
"",
0,
{0xA1, 0xC5},
true,
true,
commonMewDefinition
};
const Gen1DistributionPokemon g1_millenniumDomeMew = {
"Millennium Dome Mew",
"",
0,
{0xA1, 0xC5},
true,
true,
commonMewDefinition
};
const Gen1DistributionPokemon g1_austriaMew = {
"Austria Mew",
"AUSTRIA",
0,
{0xA1, 0xC5},
false,
true,
commonMewDefinition
};
const Gen1DistributionPokemon g1_ukIrelandPokemonChampionship2000Mew = {
"UK and Ireland Pokémon Championship 2000 Mew",
"UK",
0,
{0xA1, 0xC5},
false,
true,
commonMewDefinition
};
const Gen1DistributionPokemon g1_coraChatelineauMew = {
"Cora Châtelineau Mew",
"BENELUX",
0,
{0xA1, 0xC5},
false,
true,
commonMewDefinition
};
const Gen1DistributionPokemon g1_francePokemonTournamentMew = {
"France Pokémon Tournament Mew",
"",
0,
{0xA1, 0xC5},
true,
true,
commonMewDefinition
};
const Gen1DistributionPokemon g1_spainPokemonTournamentMew = {
"Spain Pokémon Tournament Mew",
"EUROPE",
0,
{0x64, 0x38},
false,
true,
commonMewDefinition
};
const Gen1DistributionPokemon g1_mewsFlashMew = {
"Mews Flash Mew",
"UK",
0,
{0xA1, 0xC5},
false,
true,
commonMewDefinition
};
const Gen1DistributionPokemon g1_pokemonPatrolMew = {
"Pokémon Patrol Mew",
"YOSHIBB",
0,
{0xA1, 0xC5},
false,
true,
commonMewDefinition
};
const Gen1DistributionPokemon g1_nintendoOfficialMagazineTourMew = {
"Nintendo Official Magazine Tour Mew",
"UK",
0,
{0xA1, 0xC5},
false,
true,
commonMewDefinition
};
const Gen1DistributionPokemon g1_canadianPokemonStadiumTour2000Mew = {
"Canadian Pokémon Stadium Tour 2000 Mew",
"LUIGIC",
0,
{0xA1, 0xC5},
false,
true,
commonMewDefinition
};
const Gen1DistributionPokemon g1_pokemonStadiumTour2000Mew = {
"Pokémon 2000 Stadium Tour Mew",
"LUIGE",
0,
{0xA1, 0xC5},
false,
true,
commonMewDefinition
};
const Gen1DistributionPokemon g1_canadaToysRUsMew = {
"Canada Toys \"R\" Us Mew",
"YOSHIRA",
0,
{0xA1, 0xC5},
false,
true,
commonMewDefinition
};
const Gen1DistributionPokemon g1_usToysRUsMew = {
"U.S. Toys \"R\" Us Mew",
"YOSHIBA",
0,
{0xA1, 0xC5},
false,
true,
commonMewDefinition
};
const Gen1DistributionPokemon g1_nintendoPowerMew = {
"Nintendo Power Mew",
"YOSHIRB",
1000,
{0xA1, 0xC5},
false,
true,
commonMewDefinition
};
const Gen1DistributionPokemon g1_pokemonLeagueNintendoTrainingTour99Mew = {
"Pokémon League Nintendo Training Tour '99 Mew",
"YOSHIRA",
0,
{0xA1, 0xC5},
false,
true,
commonMewDefinition
};
const Gen1DistributionPokemon g1_nintendoPowerPikachu = {
"Nintendo Power Pikachu",
"",
1000,
{0xA1, 0xC5}, // according to this thread, same IVs as distribution mew:
// https://projectpokemon.org/home/forums/topic/37431-gen-i-v-event-contributions-thread/page/5/
true,
true,
surfingPikachu
};
const Gen1DistributionPokemon g1_pokeTourMew = {
"Poké Tour Mew",
"AUS",
0,
{0xA1, 0xC5},
false,
true,
commonMewDefinition
};
const Gen1DistributionPokemon g1_pokemonPowerMew = {
"Pokémon Power Mew",
"MARIO",
151,
{0xA1, 0xC5},
false,
true,
commonMewDefinition
};
static const Gen1DistributionPokemon* mainList[] = {
&g1_nintendoUKPokemonFestivalMew2016,
&g1_clubNintendoMexicoGiveawayMew,
&g1_farMorBornMew,
&g1_christmasPresentMew,
&g1_swedenMewOnTour,
&g1_tennispalatsiMew,
&g1_norwayPokemonTourMew,
&g1_clubNintendoMew,
&g1_pokemon2000ChampionshipMew,
&g1_millenniumDomeMew,
&g1_austriaMew,
&g1_ukIrelandPokemonChampionship2000Mew,
&g1_coraChatelineauMew,
&g1_francePokemonTournamentMew,
&g1_spainPokemonTournamentMew,
&g1_mewsFlashMew,
&g1_pokemonPatrolMew,
&g1_nintendoOfficialMagazineTourMew,
&g1_canadianPokemonStadiumTour2000Mew,
&g1_pokemonStadiumTour2000Mew,
&g1_canadaToysRUsMew,
&g1_usToysRUsMew,
&g1_nintendoPowerMew,
&g1_pokemonLeagueNintendoTrainingTour99Mew,
&g1_nintendoPowerPikachu,
&g1_pokeTourMew,
&g1_pokemonPowerMew
};
void gen1_getMainDistributionPokemonList(const Gen1DistributionPokemon**& outList, uint32_t& outSize)
{
outList = mainList;
outSize = sizeof(mainList) / sizeof(mainList[0]);
}

555
src/gen1/Gen1GameReader.cpp Normal file
View File

@ -0,0 +1,555 @@
#include "gen1/Gen1GameReader.h"
#include "gen1/Gen1SpriteDecoder.h"
#include "RomReader.h"
#include "SaveManager.h"
#include "utils.h"
#include <cstdlib>
/**
* @brief This function calculates the main data checksum
*/
uint8_t calculateMainDataChecksum(ISaveManager& saveManager)
{
Gen1Checksum checksum;
const uint16_t checksummedDataStart = 0x598;
const uint16_t checksummedDataEnd = 0x1523;
const uint16_t numBytes = checksummedDataEnd - checksummedDataStart;
uint16_t i;
uint8_t byte;
saveManager.seekToBankOffset(1, checksummedDataStart);
for(i=0; i < numBytes; ++i)
{
saveManager.readByte(byte);
checksum.addByte(byte);
}
return checksum.get();
}
uint8_t calculateWholeBoxBankChecksum(ISaveManager& saveManager, uint8_t bankIndex)
{
Gen1Checksum checksum;
const uint16_t checksummedDataStart = 0x0;
const uint16_t checksummedDataEnd = 0x1A4C;
const uint16_t numBytes = checksummedDataEnd - checksummedDataStart;
uint16_t i;
uint8_t byte;
saveManager.seekToBankOffset(bankIndex, checksummedDataStart);
for(i=0; i < numBytes; ++i)
{
saveManager.readByte(byte);
checksum.addByte(byte);
}
return checksum.get();
}
Gen1GameReader::Gen1GameReader(IRomReader &romReader, ISaveManager &saveManager, Gen1GameType gameType)
: romReader_(romReader)
, saveManager_(saveManager)
, spriteDecoder_(romReader_)
, gameType_(gameType)
{
}
const char *Gen1GameReader::getPokemonName(uint8_t index) const
{
// based on: https://github.com/seanmorris/pokemon-parser/blob/master/source/PokemonRom.js#L493
static char result[20];
uint8_t encodedText[0xA];
uint32_t numRead;
uint16_t pointer;
if(gameType_ == Gen1GameType::BLUE || gameType_ == Gen1GameType::RED)
{
romReader_.seek(0x2FA3);
const uint8_t bankByte = romReader_.peek();
romReader_.seek(0x2FAE);
romReader_.readUint16(pointer);
// seek to the right location
romReader_.seekToRomPointer(pointer, bankByte);
}
else
{
// Pkmn Yellow
romReader_.seek(0xE8000);
}
romReader_.advance((index - 1) * 0xA);
// max 10 bytes
numRead = romReader_.readUntil(encodedText, 0x50, 0xA);
gen1_decodePokeText(encodedText, numRead, result, sizeof(result));
return result;
}
uint8_t Gen1GameReader::getPokemonNumber(uint8_t index) const
{
// Based on https://github.com/seanmorris/pokemon-parser/blob/master/source/PokemonRom.js#L509
uint8_t result = 0xFF;
const uint32_t romOffset = (gameType_ == Gen1GameType::YELLOW) ? 0x410B1 : 0x41024;
romReader_.seek(romOffset + (index - 1));
romReader_.readByte(result);
return result;
}
bool Gen1GameReader::isValidIndex(uint8_t index) const
{
switch(index)
{
case 0x0:
case 0x1F:
case 0x20:
case 0x32:
case 0x34:
case 0x38:
case 0x3D:
case 0x3E:
case 0x3F:
case 0x43:
case 0x44:
case 0x45:
case 0x4F:
case 0x50:
case 0x51:
case 0x56:
case 0x57:
case 0x5E:
case 0x5F:
case 0x73:
case 0x79:
case 0x7A:
case 0x7F:
case 0x86:
case 0x87:
case 0x89:
case 0x8C:
case 0x92:
case 0x9C:
case 0x9F:
case 0xA0:
case 0xA1:
case 0xA2:
case 0xAC:
case 0xAE:
case 0xAF:
case 0xB5:
case 0xB6:
case 0xB7:
case 0xB8:
// Invalid (MISSINGNO) entry
return false;
default:
break;
}
return (index < 192);
}
bool Gen1GameReader::readPokemonStatsForIndex(uint8_t index, Gen1PokeStats &outStats) const
{
// Strangely enough, the pokemon stats data is not stored by index, but by pokedex number.
// So we need to do a conversion here
// Based on: https://bulbapedia.bulbagarden.net/wiki/Pok%C3%A9mon_species_data_structure_(Generation_I)#:~:text=The%20Pok%C3%A9mon%20species%20data%20structure,order%20rather%20than%20index%20number.
// https://github.com/seanmorris/pokemon-parser/blob/master/source/PokemonRom.js#L516
const uint8_t statsStructSize = 28;
const uint8_t pokeNumber = getPokemonNumber(index);
uint8_t spriteBank;
uint8_t statsBank;
uint16_t statsPointer;
if (gameType_ != Gen1GameType::YELLOW && index == 0x15)
{
spriteBank = 0x1;
}
else if (index == 0xB6)
{
spriteBank = 0xB;
}
else if (index < 0x1F)
{
spriteBank = 0x9;
}
else if (index < 0x4A)
{
spriteBank = 0xA;
}
else if (index < 0x74)
{
spriteBank = 0xB;
}
else if (index < 0x99)
{
spriteBank = 0xC;
}
else
{
spriteBank = 0xD;
}
if(gameType_ != Gen1GameType::YELLOW)
{
// Mew (of which the pokenumber is 151) stats are stored at a completely different location in the rom than the rest
if (pokeNumber != 151)
{
romReader_.seek(0x153B);
romReader_.readByte(statsBank);
romReader_.seek(0x1578);
romReader_.readUint16(statsPointer);
}
else
{
// mew stats
romReader_.seek(0x159C);
romReader_.readByte(statsBank);
romReader_.seek(0x1593);
romReader_.readUint16(statsPointer);
}
romReader_.seekToRomPointer(statsPointer, statsBank);
if (pokeNumber != 151)
{
// the number is 1-based.
romReader_.advance(statsStructSize * (pokeNumber - 1));
}
}
else
{
// Dealing with Pokemon yellow
romReader_.seek(0x383DE);
// the number is 1-based.
romReader_.advance(statsStructSize * (pokeNumber - 1));
}
romReader_.readByte(outStats.pokedex_number);
romReader_.readByte(outStats.base_hp);
romReader_.readByte(outStats.base_attack);
romReader_.readByte(outStats.base_defense);
romReader_.readByte(outStats.base_speed);
romReader_.readByte(outStats.base_special);
romReader_.readByte(outStats.type1);
romReader_.readByte(outStats.type2);
romReader_.readByte(outStats.catch_rate);
romReader_.readByte(outStats.base_exp_yield);
romReader_.readByte(outStats.front_sprite_dimensions);
outStats.sprite_bank = spriteBank;
romReader_.readUint16(outStats.pointer_to_frontsprite);
romReader_.readUint16(outStats.pointer_to_backsprite);
romReader_.read(outStats.lvl1_attacks, 4);
romReader_.readByte(outStats.growth_rate);
romReader_.read(outStats.tm_hm_flags, 7);
return true;
}
uint8_t Gen1GameReader::getColorPaletteIndexByPokemonNumber(uint8_t pokeNumber)
{
uint8_t result;
// pokeyellow.map from https://github.com/pret/pokeyellow (after compilation)
const uint32_t romOffset = (gameType_ == Gen1GameType::YELLOW) ? 0x72922 : 0x725C9;
if(!romReader_.seek(romOffset + (pokeNumber - 1)))
{
return 0xFF;
}
if(!romReader_.readByte(result))
{
result = 0xFF;
}
return result;
}
void Gen1GameReader::readColorPalette(uint8_t paletteId, uint16_t* outColorPalette)
{
uint16_t* cur = outColorPalette;
const uint16_t* end = cur + 4;
uint8_t byte1;
uint8_t byte2;
// based on https://datacrystal.romhacking.net/wiki/Pok%C3%A9mon_Red_and_Blue/ROM_map
// and https://bulbapedia.bulbagarden.net/wiki/List_of_color_palettes_by_index_number_(Generation_I)
const uint32_t romOffset = (gameType_ == Gen1GameType::YELLOW) ? 0x72AF9 : 0x72660;
romReader_.seek(romOffset + (paletteId * 8));
while(cur < end)
{
romReader_.readByte(byte1);
romReader_.readByte(byte2);
// stored in little endian -> least significant bits of the 16 bit value are stored first
(*cur) = (((uint16_t)byte2 << 8) | byte1);
++cur;
}
}
const char *Gen1GameReader::getTrainerName() const
{
static char result[20];
uint8_t encodedPlayerName[0xB];
saveManager_.seek(0x2598);
saveManager_.readUntil(encodedPlayerName, 0x50, 0xB);
gen1_decodePokeText(encodedPlayerName, sizeof(encodedPlayerName), result, sizeof(result));
return result;
}
const char *Gen1GameReader::getRivalName() const
{
static char result[20];
uint8_t encodedRivalName[0xB];
saveManager_.seek(0x25F6);
saveManager_.readUntil(encodedRivalName, 0x50, sizeof(encodedRivalName));
gen1_decodePokeText(encodedRivalName, sizeof(encodedRivalName), result, sizeof(result));
return result;
}
uint16_t Gen1GameReader::getTrainerID() const
{
uint16_t result;
saveManager_.seek(0x2605);
saveManager_.readUint16(result);
return result;
}
Gen1Party Gen1GameReader::getParty()
{
return Gen1Party((*this), saveManager_);
}
Gen1Box Gen1GameReader::getBox(uint8_t boxIndex)
{
return Gen1Box((*this), saveManager_, boxIndex);
}
uint8_t Gen1GameReader::getCurrentBoxIndex()
{
uint8_t byte;
saveManager_.seek(0x284C);
saveManager_.readByte(byte);
return byte & 0x3F;
}
bool Gen1GameReader::getPokedexFlag(PokedexFlag dexFlag, uint8_t pokedexNumber) const
{
const uint16_t saveOffset = (dexFlag == POKEDEX_SEEN) ? 0x25B6 : 0x25A3;
uint8_t byte;
if(pokedexNumber < 1 || pokedexNumber > 151)
{
return false;
}
// for the next operations, the pokedexNumber will be used as a zero-based value
--pokedexNumber;
saveManager_.seek(saveOffset + (pokedexNumber / 8));
saveManager_.readByte(byte);
// in this case, bits are ordered within bytes from lowest to highest
// source: https://bulbapedia.bulbagarden.net/wiki/Save_data_structure_(Generation_I)#bank1_main_pokedex
return (byte >> (pokedexNumber % 8)) & 0x1;
}
void Gen1GameReader::setPokedexFlag(PokedexFlag dexFlag, uint8_t pokedexNumber) const
{
const uint16_t saveOffset = (dexFlag == POKEDEX_SEEN) ? 0x25B6 : 0x25A3;
uint8_t byte;
if(pokedexNumber < 1 || pokedexNumber > 151)
{
return;
}
// for the next operations, the pokedexNumber will be used as a zero-based value
--pokedexNumber;
saveManager_.seek(saveOffset + (pokedexNumber / 8));
byte = saveManager_.peek();
// in this case, bits are ordered within bytes from lowest to highest
// source: https://bulbapedia.bulbagarden.net/wiki/Save_data_structure_(Generation_I)#bank1_main_pokedex
byte |= (1 << (pokedexNumber % 8));
saveManager_.writeByte(byte);
}
uint8_t Gen1GameReader::getPokedexCounter(PokedexFlag dexFlag) const
{
const uint16_t saveOffset = (dexFlag == POKEDEX_SEEN) ? 0x25B6 : 0x25A3;
uint8_t bytes[19];
uint8_t result = 0;
saveManager_.seek(saveOffset);
saveManager_.read(bytes, sizeof(bytes));
const uint8_t *cur = bytes;
const uint8_t *const end = bytes + sizeof(bytes);
uint8_t bit = 0;
while(cur < end)
{
bit = 0;
// while we're not at the last bit yet, walk through all the bits and add to counter if set
while(bit != 0x80)
{
if(bit == 0)
{
bit = 1;
}
else
{
bit <<= 1;
}
if((*cur) & bit)
{
++result;
}
}
++cur;
}
return result;
}
uint8_t* Gen1GameReader::decodeSprite(uint8_t bankIndex, uint16_t pointer)
{
return spriteDecoder_.decode(bankIndex, pointer);
}
uint8_t Gen1GameReader::addPokemon(Gen1TrainerPokemon& poke, const char* originalTrainerID, const char* nickname)
{
Gen1Party party = getParty();
uint8_t result = 0xFF;
const uint8_t pokedexNumber = getPokemonNumber(poke.poke_index);
if(!originalTrainerID)
{
originalTrainerID = getTrainerName();
}
if(party.getNumberOfPokemon() < party.getMaxNumberOfPokemon())
{
party.add(poke, originalTrainerID, nickname);
result = 0xFE;
}
else
{
const uint8_t currentBoxIndex = getCurrentBoxIndex();
for(uint8_t i = 0; i < 12; ++i)
{
Gen1Box box = getBox(i);
if(box.getNumberOfPokemon() == box.getMaxNumberOfPokemon())
{
continue;
}
box.add(poke, originalTrainerID, nickname);
result = i;
updateWholeBoxBankChecksum(getGen1BoxBankIndex(i, currentBoxIndex));
}
}
setPokedexFlag(PokedexFlag::POKEDEX_SEEN, pokedexNumber);
setPokedexFlag(PokedexFlag::POKEDEX_OWNED, pokedexNumber);
updateMainChecksum();
return result;
}
uint8_t Gen1GameReader::addDistributionPokemon(const Gen1DistributionPokemon& distributionPoke, const char* nickname)
{
Gen1TrainerPokemon poke = distributionPoke.poke;
const char* originalTrainerName;
if(distributionPoke.setPlayerAsOriginalTrainer)
{
originalTrainerName = getTrainerName();
poke.original_trainer_ID = getTrainerID();
}
else
{
originalTrainerName = distributionPoke.originalTrainer;
if(distributionPoke.regenerateTrainerID)
{
if(distributionPoke.originalTrainerID)
{
// limit set, apply it
poke.original_trainer_ID = (uint16_t)(rand() % distributionPoke.originalTrainerID);
}
else
{
// no limit. The max is the max of the uint16_t type
poke.original_trainer_ID =(uint16_t)(rand() % UINT16_MAX);
}
}
else
{
poke.original_trainer_ID = distributionPoke.originalTrainerID;
}
}
poke.iv_data[0] = distributionPoke.iv_data[0];
poke.iv_data[1] = distributionPoke.iv_data[1];
return addPokemon(poke, originalTrainerName, nickname);
}
bool Gen1GameReader::isMainChecksumValid()
{
const uint16_t mainDataChecksumOffset = 0x3523;
uint8_t storedChecksum;
uint8_t calculatedChecksum;
saveManager_.seek(mainDataChecksumOffset);
saveManager_.readByte(storedChecksum);
calculatedChecksum = calculateMainDataChecksum(saveManager_);
return (storedChecksum == calculatedChecksum);
}
void Gen1GameReader::updateMainChecksum()
{
const uint16_t mainDataChecksumOffset = 0x3523;
const uint8_t calculatedChecksum = calculateMainDataChecksum(saveManager_);
saveManager_.seek(mainDataChecksumOffset);
saveManager_.writeByte(calculatedChecksum);
}
bool Gen1GameReader::isWholeBoxBankValid(uint8_t bankIndex)
{
const uint16_t wholeBankChecksumOffset = 0x1A4C;
uint8_t storedChecksum;
uint8_t calculatedChecksum;
saveManager_.seekToBankOffset(bankIndex, wholeBankChecksumOffset);
saveManager_.readByte(storedChecksum);
calculatedChecksum = calculateWholeBoxBankChecksum(saveManager_, bankIndex);
return (storedChecksum == calculatedChecksum);
}
void Gen1GameReader::updateWholeBoxBankChecksum(uint8_t bankIndex)
{
const uint16_t wholeBankChecksumOffset = 0x1A4C;
if(bankIndex < 2 || bankIndex > 3)
{
// only valid/needed for bank 2 and 3
return;
}
const uint8_t calculatedChecksum = calculateWholeBoxBankChecksum(saveManager_, bankIndex);
saveManager_.seekToBankOffset(bankIndex, wholeBankChecksumOffset);
saveManager_.writeByte(calculatedChecksum);
}

View File

@ -0,0 +1,547 @@
#include "gen1/Gen1PlayerPokemonStorage.h"
#include "gen1/Gen1GameReader.h"
#include "gen1/Gen1Common.h"
#include "SaveManager.h"
#include "utils.h"
#include <cstring>
typedef struct Gen1TrainerPartyMeta
{
uint8_t number_of_pokemon;
uint8_t species_index_list[7];
} Gen1TrainerPartyMeta;
typedef struct Gen1TrainerBoxMeta
{
uint8_t number_of_pokemon;
uint8_t species_index_list[21];
} Gen1TrainerBoxMeta;
static const uint8_t NICKNAME_SIZE = 0xB;
static const uint8_t OT_NAME_SIZE = 0xB;
/**
* @brief This function will load the metadata of the trainer party into the specified outPartyMeta variable.
* Note that it won't contain the detailed data about the pokemon in the party.
*/
static bool getPartyMetadata(ISaveManager& saveManager, Gen1TrainerPartyMeta &outPartyMeta)
{
saveManager.seek(0x2F2C);
saveManager.readByte(outPartyMeta.number_of_pokemon);
saveManager.read(outPartyMeta.species_index_list, 6);
outPartyMeta.species_index_list[6] = 0xFF;
return true;
}
static void writePartyMetadata(ISaveManager& saveManager, Gen1TrainerPartyMeta& partyMeta)
{
saveManager.seek(0x2F2C);
saveManager.writeByte(partyMeta.number_of_pokemon);
saveManager.write(partyMeta.species_index_list, 6);
saveManager.writeByte(0xFF);
}
uint8_t getGen1BoxBankIndex(uint8_t boxIndex, uint8_t currentBoxIndex)
{
// NOTE: important: current box data to the "longterm" bank (in bank 2 or 3) until you switch the current box in the pc
// found this while experimenting with a Pkmn Blue save saved close to the start of the game by transferring a ratata to box 1
// the current box is stored in bank 1
if(boxIndex == currentBoxIndex)
{
return 1;
}
return (boxIndex < 6) ? 2 : 3;
}
static uint16_t getBoxBankOffset(uint8_t boxIndex, uint8_t currentBoxIndex)
{
const uint16_t boxSize = 0x462;
// NOTE: important: current box data to the "longterm" bank (in bank 2 or 3) until you switch the current box in the pc
// found this while experimenting with a Pkmn Blue save saved close to the start of the game by transferring a ratata to box 1
// the current box is stored at this offset
if(boxIndex == currentBoxIndex)
{
return 0x10C0;
}
return (boxIndex % 6) * boxSize;
}
static void readCommonPokeData(ISaveManager& saveManager, Gen1TrainerPokemon& outTrainerPokemon)
{
memset(&outTrainerPokemon, 0, sizeof(Gen1TrainerPokemon));
saveManager.readByte(outTrainerPokemon.poke_index);
// stored in big endian form
saveManager.readUint16(outTrainerPokemon.current_hp, Endianness::BIG);
saveManager.readByte(outTrainerPokemon.level);
saveManager.readByte(outTrainerPokemon.status_condition);
saveManager.readByte(outTrainerPokemon.type1);
saveManager.readByte(outTrainerPokemon.type2);
saveManager.readByte(outTrainerPokemon.catch_rate_or_held_item);
saveManager.readByte(outTrainerPokemon.index_move1);
saveManager.readByte(outTrainerPokemon.index_move2);
saveManager.readByte(outTrainerPokemon.index_move3);
saveManager.readByte(outTrainerPokemon.index_move4);
saveManager.readUint16(outTrainerPokemon.original_trainer_ID);
// Testing with a save revealed that these fields are stored in Big endian form
saveManager.readUint24(outTrainerPokemon.exp, Endianness::BIG);
saveManager.readUint16(outTrainerPokemon.hp_effort_value, Endianness::BIG);
saveManager.readUint16(outTrainerPokemon.atk_effort_value, Endianness::BIG);
saveManager.readUint16(outTrainerPokemon.def_effort_value, Endianness::BIG);
saveManager.readUint16(outTrainerPokemon.speed_effort_value, Endianness::BIG);
saveManager.readUint16(outTrainerPokemon.special_effort_value, Endianness::BIG);
// no endianness stuff for IV. Make sure to keep as is
saveManager.read(outTrainerPokemon.iv_data, 2);
saveManager.readByte(outTrainerPokemon.pp_move1);
saveManager.readByte(outTrainerPokemon.pp_move2);
saveManager.readByte(outTrainerPokemon.pp_move3);
saveManager.readByte(outTrainerPokemon.pp_move4);
}
static void writeCommonPokeData(ISaveManager& saveManager, const Gen1TrainerPokemon& trainerPokemon)
{
saveManager.writeByte(trainerPokemon.poke_index);
saveManager.writeUint16(trainerPokemon.current_hp, Endianness::BIG);
saveManager.writeByte(trainerPokemon.level);
saveManager.writeByte(trainerPokemon.status_condition);
saveManager.writeByte(trainerPokemon.type1);
saveManager.writeByte(trainerPokemon.type2);
saveManager.writeByte(trainerPokemon.catch_rate_or_held_item);
saveManager.writeByte(trainerPokemon.index_move1);
saveManager.writeByte(trainerPokemon.index_move2);
saveManager.writeByte(trainerPokemon.index_move3);
saveManager.writeByte(trainerPokemon.index_move4);
saveManager.writeUint16(trainerPokemon.original_trainer_ID);
// Testing with a save revealed that these fields are stored in Big endian form
saveManager.writeUint24(trainerPokemon.exp, Endianness::BIG);
saveManager.writeUint16(trainerPokemon.hp_effort_value, Endianness::BIG);
saveManager.writeUint16(trainerPokemon.atk_effort_value, Endianness::BIG);
saveManager.writeUint16(trainerPokemon.def_effort_value, Endianness::BIG);
saveManager.writeUint16(trainerPokemon.speed_effort_value, Endianness::BIG);
saveManager.writeUint16(trainerPokemon.special_effort_value, Endianness::BIG);
// no endianness stuff for IV. Make sure to keep as is
saveManager.write(trainerPokemon.iv_data, 2);
saveManager.writeByte(trainerPokemon.pp_move1);
saveManager.writeByte(trainerPokemon.pp_move2);
saveManager.writeByte(trainerPokemon.pp_move3);
saveManager.writeByte(trainerPokemon.pp_move4);
}
/**
* @brief This function will load the metadata of the trainer box into the specified outBoxMeta variable.
* Note that it won't contain the detailed data about the pokemon in the party.
*/
static bool getBoxMetadata(ISaveManager& saveManager, uint8_t boxIndex, uint8_t currentBoxIndex, Gen1TrainerBoxMeta& outBoxMeta)
{
saveManager.seekToBankOffset(getGen1BoxBankIndex(boxIndex, currentBoxIndex), getBoxBankOffset(boxIndex, currentBoxIndex));
saveManager.readByte(outBoxMeta.number_of_pokemon);
saveManager.read(outBoxMeta.species_index_list, 20);
outBoxMeta.species_index_list[20] = 0xFF;
// if seems the number_of_pokemon field read is unreliable if the box hasn't been used before.
// fortunately we can also use the species index list to determine the number
// note, you may wonder why we do the readByte for that field in the first place then, don't you?
// well, we still need to advance the pointer in the buffer in SaveManager. So we might as well
// 0xFF is used as a terminator after the last entry in the list
outBoxMeta.number_of_pokemon = 0;
uint8_t* cur = outBoxMeta.species_index_list;
const uint8_t* const end = cur + sizeof(outBoxMeta.species_index_list);
while(cur < end)
{
if((*cur) == 0xFF)
{
//terminator encountered. Stop looping
break;
}
++outBoxMeta.number_of_pokemon;
++cur;
}
return true;
}
static void writeBoxMetadata(ISaveManager& saveManager, uint8_t boxIndex, uint8_t currentBoxIndex, const Gen1TrainerBoxMeta& boxMeta)
{
saveManager.seekToBankOffset(getGen1BoxBankIndex(boxIndex, currentBoxIndex), getBoxBankOffset(boxIndex, currentBoxIndex));
saveManager.writeByte(boxMeta.number_of_pokemon);
saveManager.write(boxMeta.species_index_list, 20);
saveManager.writeByte(0xFF);
}
static uint8_t calculateBoxChecksum(ISaveManager& saveManager, uint8_t boxIndex, uint8_t currentBoxIndex)
{
Gen1Checksum checksum;
const uint16_t boxSize = 0x462;
uint16_t i;
uint8_t byte;
saveManager.seekToBankOffset(getGen1BoxBankIndex(boxIndex, currentBoxIndex), getBoxBankOffset(boxIndex, currentBoxIndex));
for(i = 0; i < boxSize; ++i)
{
saveManager.readByte(byte);
checksum.addByte(byte);
}
return checksum.get();
}
Gen1Party::Gen1Party(Gen1GameReader& gameReader, ISaveManager& savManager)
: gameReader_(gameReader)
, saveManager_(savManager)
{
}
Gen1Party::~Gen1Party()
{
}
bool Gen1Party::getPokemon(uint8_t partyIndex, Gen1TrainerPokemon& outTrainerPokemon)
{
Gen1PokeStats stats;
const uint8_t PARTY_POKEMON_NUM_BYTES = 44;
const uint8_t FIRST_POKE_STRUCT_OFFSET = 8;
saveManager_.seek(0x2F2C + FIRST_POKE_STRUCT_OFFSET + ((partyIndex)*PARTY_POKEMON_NUM_BYTES));
readCommonPokeData(saveManager_, outTrainerPokemon);
// according to https://bulbapedia.bulbagarden.net/wiki/Pok%C3%A9mon_data_structure_(Generation_I)
// the level is stored a second time here for some reason. Ignore it.
saveManager_.advance();
saveManager_.readUint16(outTrainerPokemon.max_hp, Endianness::BIG);
saveManager_.readUint16(outTrainerPokemon.atk, Endianness::BIG);
saveManager_.readUint16(outTrainerPokemon.def, Endianness::BIG);
saveManager_.readUint16(outTrainerPokemon.speed, Endianness::BIG);
saveManager_.readUint16(outTrainerPokemon.special, Endianness::BIG);
// the level field above is kinda unreliable. the only reliable way is to base it on the exp field
gameReader_.readPokemonStatsForIndex(outTrainerPokemon.poke_index, stats);
outTrainerPokemon.level = getLevelForExp(outTrainerPokemon.exp, stats.growth_rate);
return true;
}
uint8_t Gen1Party::getNumberOfPokemon()
{
Gen1TrainerPartyMeta partyMeta;
if(!getPartyMetadata(saveManager_, partyMeta))
{
return 0;
}
return partyMeta.number_of_pokemon;
}
uint8_t Gen1Party::getMaxNumberOfPokemon()
{
return 6;
}
const char* Gen1Party::getPokemonNickname(uint8_t partyIndex)
{
static char result[20];
uint8_t encodedNickName[NICKNAME_SIZE];
const uint16_t FIRST_NICKNAME_NAME_OFFSET = 0x152;
saveManager_.seek(0x2F2C + FIRST_NICKNAME_NAME_OFFSET + (partyIndex * NICKNAME_SIZE));
saveManager_.readUntil(encodedNickName, 0x50, NICKNAME_SIZE);
gen1_decodePokeText(encodedNickName, sizeof(encodedNickName), result, sizeof(result));
return result;
}
void Gen1Party::setPokemonNickname(uint8_t partyIndex, const char* name)
{
const uint16_t FIRST_NICKNAME_NAME_OFFSET = 0x152;
uint8_t encodedNickName[NICKNAME_SIZE];
if(!name)
{
Gen1TrainerPokemon poke;
getPokemon(partyIndex, poke);
name = gameReader_.getPokemonName(poke.poke_index);
}
const uint16_t encodedLength = gen1_encodePokeText(name, strlen(name), encodedNickName, NICKNAME_SIZE, 0x50);
saveManager_.seek(0x2F2C + FIRST_NICKNAME_NAME_OFFSET + (partyIndex * NICKNAME_SIZE));
saveManager_.write(encodedNickName, encodedLength);
}
const char* Gen1Party::getOriginalTrainerOfPokemon(uint8_t partyIndex)
{
static char result[20];
uint8_t encodedOTName[OT_NAME_SIZE];
const uint16_t FIRST_OT_NAME_OFFSET = 0x110;
saveManager_.seek(0x2F2C + FIRST_OT_NAME_OFFSET + (partyIndex * OT_NAME_SIZE));
saveManager_.readUntil(encodedOTName, 0x50, OT_NAME_SIZE);
gen1_decodePokeText(encodedOTName, sizeof(encodedOTName), result, sizeof(result));
return result;
}
void Gen1Party::setOriginalTrainerOfPokemon(uint8_t partyIndex, const char* originalTrainer)
{
uint8_t encodedOTName[OT_NAME_SIZE];
const uint16_t FIRST_OT_NAME_OFFSET = 0x110;
const uint16_t encodedLength = gen1_encodePokeText(originalTrainer, strlen(originalTrainer), encodedOTName, OT_NAME_SIZE, 0x50);
saveManager_.seek(0x2F2C + FIRST_OT_NAME_OFFSET + (partyIndex * OT_NAME_SIZE));
saveManager_.write(encodedOTName, encodedLength);
}
bool Gen1Party::add(Gen1TrainerPokemon& poke, const char* originalTrainerID, const char* nickname)
{
const uint8_t PARTY_POKEMON_NUM_BYTES = 44;
const uint8_t FIRST_POKE_STRUCT_OFFSET = 8;
Gen1TrainerPartyMeta partyMeta;
getPartyMetadata(saveManager_, partyMeta);
if(partyMeta.number_of_pokemon >= 6)
{
// no room for this pokemon
return false;
}
const uint8_t partyIndex = partyMeta.number_of_pokemon;
partyMeta.species_index_list[partyIndex] = poke.poke_index;
++partyMeta.number_of_pokemon;
if(partyMeta.number_of_pokemon != 6)
{
// we need to add a terminator
partyMeta.species_index_list[partyMeta.number_of_pokemon] = 0xFF;
}
writePartyMetadata(saveManager_, partyMeta);
// make sure the stat fields are filled in by recalculating them.
// this is the same as what happens when withdrawing them from an ingame PC box
gen1_recalculatePokeStats(gameReader_, poke);
poke.current_hp = poke.max_hp;
saveManager_.seek(0x2F2C + FIRST_POKE_STRUCT_OFFSET + ((partyIndex)*PARTY_POKEMON_NUM_BYTES));
writeCommonPokeData(saveManager_, poke);
// according to https://bulbapedia.bulbagarden.net/wiki/Pok%C3%A9mon_data_structure_(Generation_I)
// the level is stored a second time here for some reason.
saveManager_.writeByte(poke.level);
saveManager_.writeUint16(poke.max_hp, Endianness::BIG);
saveManager_.writeUint16(poke.atk, Endianness::BIG);
saveManager_.writeUint16(poke.def, Endianness::BIG);
saveManager_.writeUint16(poke.speed, Endianness::BIG);
saveManager_.writeUint16(poke.special, Endianness::BIG);
// now store the original trainer string and name/nickname
setOriginalTrainerOfPokemon(partyIndex, originalTrainerID);
setPokemonNickname(partyIndex, nickname);
return true;
}
Gen1Box::Gen1Box(Gen1GameReader& gameReader, ISaveManager& saveManager, uint8_t boxIndex)
: gameReader_(gameReader)
, saveManager_(saveManager)
, boxIndex_(boxIndex)
{
}
Gen1Box::~Gen1Box()
{
}
bool Gen1Box::getPokemon(uint8_t index, Gen1TrainerPokemon& outTrainerPokemon)
{
Gen1PokeStats stats;
const uint8_t BOX_POKEMON_NUM_BYTES = 33;
const uint8_t FIRST_POKE_STRUCT_OFFSET = 22;
const uint8_t currentBoxIndex = gameReader_.getCurrentBoxIndex();
const uint8_t bankIndex = getGen1BoxBankIndex(boxIndex_, currentBoxIndex);
const uint16_t boxOffset = getBoxBankOffset(boxIndex_, currentBoxIndex);
saveManager_.seekToBankOffset(bankIndex, boxOffset + FIRST_POKE_STRUCT_OFFSET + (index * BOX_POKEMON_NUM_BYTES));
readCommonPokeData(saveManager_, outTrainerPokemon);
// the level field above is kinda unreliable. the only reliable way is to base it on the exp field
gameReader_.readPokemonStatsForIndex(outTrainerPokemon.poke_index, stats);
outTrainerPokemon.level = getLevelForExp(outTrainerPokemon.exp, stats.growth_rate);
return true;
}
uint8_t Gen1Box::getNumberOfPokemon()
{
Gen1TrainerBoxMeta boxMeta;
const uint8_t currentBoxIndex = gameReader_.getCurrentBoxIndex();
if(!getBoxMetadata(saveManager_, boxIndex_, currentBoxIndex, boxMeta))
{
return 0;
}
return boxMeta.number_of_pokemon;
}
uint8_t Gen1Box::getMaxNumberOfPokemon()
{
return 20;
}
const char* Gen1Box::getPokemonNickname(uint8_t index)
{
static char result[20];
uint8_t encodedNickName[NICKNAME_SIZE];
const uint16_t nicknameOffset = 0x386;
const uint8_t currentBoxIndex = gameReader_.getCurrentBoxIndex();
const uint8_t bankIndex = getGen1BoxBankIndex(boxIndex_, currentBoxIndex);
const uint16_t boxOffset = getBoxBankOffset(boxIndex_, currentBoxIndex);
saveManager_.seekToBankOffset(bankIndex, boxOffset + nicknameOffset + (index * NICKNAME_SIZE));
saveManager_.readUntil(encodedNickName, 0x50, NICKNAME_SIZE);
gen1_decodePokeText(encodedNickName, sizeof(encodedNickName), result, sizeof(result));
return result;
}
void Gen1Box::setPokemonNickname(uint8_t index, const char* name)
{
uint8_t encodedNickName[NICKNAME_SIZE];
const uint16_t nicknameOffset = 0x386;
const uint8_t currentBoxIndex = gameReader_.getCurrentBoxIndex();
const uint8_t bankIndex = getGen1BoxBankIndex(boxIndex_, currentBoxIndex);
const uint16_t boxOffset = getBoxBankOffset(boxIndex_, currentBoxIndex);
if(!name)
{
Gen1TrainerPokemon poke;
getPokemon(index, poke);
name = gameReader_.getPokemonName(poke.poke_index);
}
const uint16_t encodedLength = gen1_encodePokeText(name, strlen(name), encodedNickName, NICKNAME_SIZE, 0x50);
saveManager_.seekToBankOffset(bankIndex, boxOffset + nicknameOffset + (index * NICKNAME_SIZE));
saveManager_.write(encodedNickName, encodedLength);
}
const char* Gen1Box::getOriginalTrainerOfPokemon(uint8_t index)
{
static char result[20];
uint8_t encodedOTName[OT_NAME_SIZE];
const uint16_t otOffset = 0x2AA;
const uint8_t currentBoxIndex = gameReader_.getCurrentBoxIndex();
const uint8_t bankIndex = getGen1BoxBankIndex(boxIndex_, currentBoxIndex);
const uint16_t boxOffset = getBoxBankOffset(boxIndex_, currentBoxIndex);
saveManager_.seekToBankOffset(bankIndex, boxOffset + otOffset + (index * OT_NAME_SIZE));
saveManager_.readUntil(encodedOTName, 0x50, OT_NAME_SIZE);
gen1_decodePokeText(encodedOTName, sizeof(encodedOTName), result, sizeof(result));
return result;
}
void Gen1Box::setOriginalTrainerOfPokemon(uint8_t index, const char* originalTrainer)
{
uint8_t encodedOTName[OT_NAME_SIZE];
const uint16_t otOffset = 0x2AA;
const uint8_t currentBoxIndex = gameReader_.getCurrentBoxIndex();
const uint8_t bankIndex = getGen1BoxBankIndex(boxIndex_, currentBoxIndex);
const uint16_t boxOffset = getBoxBankOffset(boxIndex_, currentBoxIndex);
const uint16_t encodedLength = gen1_encodePokeText(originalTrainer, strlen(originalTrainer), encodedOTName, OT_NAME_SIZE, 0x50);
saveManager_.seekToBankOffset(bankIndex, boxOffset + otOffset + (index * OT_NAME_SIZE));
saveManager_.write(encodedOTName, encodedLength);
}
bool Gen1Box::add(Gen1TrainerPokemon& poke, const char* originalTrainerID, const char* nickname)
{
const uint8_t BOX_POKEMON_NUM_BYTES = 33;
const uint8_t FIRST_POKE_STRUCT_OFFSET = 22;
Gen1TrainerBoxMeta boxMeta;
const uint8_t currentBoxIndex = gameReader_.getCurrentBoxIndex();
getBoxMetadata(saveManager_, boxIndex_, currentBoxIndex, boxMeta);
if(boxMeta.number_of_pokemon >= 6)
{
// no room for this pokemon
return false;
}
const uint8_t index = boxMeta.number_of_pokemon;
const uint8_t bankIndex = getGen1BoxBankIndex(boxIndex_, currentBoxIndex);
const uint16_t boxOffset = getBoxBankOffset(boxIndex_, currentBoxIndex);
boxMeta.species_index_list[index] = poke.poke_index;
++boxMeta.number_of_pokemon;
if(boxMeta.number_of_pokemon != 6)
{
// we need to add a terminator
boxMeta.species_index_list[boxMeta.number_of_pokemon] = 0xFF;
}
// make sure the stat fields are filled in by recalculating them.
// this is the same as what happens when withdrawing them from an ingame PC box
gen1_recalculatePokeStats(gameReader_, poke);
poke.current_hp = poke.max_hp;
writeBoxMetadata(saveManager_, boxIndex_, currentBoxIndex, boxMeta);
saveManager_.seekToBankOffset(bankIndex, boxOffset + FIRST_POKE_STRUCT_OFFSET + (index * BOX_POKEMON_NUM_BYTES));
writeCommonPokeData(saveManager_, poke);
// now store the original trainer string and name/nickname
setOriginalTrainerOfPokemon(index, originalTrainerID);
setPokemonNickname(index, nickname);
// now update the checksum. Note that this is NOT the only checksum that will need to be updated. There's also the whole bank checksum.
// this needs to be updated by Gen1GameReader once it is done manipulating the boxes
updateChecksum(currentBoxIndex);
return true;
}
bool Gen1Box::isChecksumValid(uint8_t currentBoxIndex)
{
uint8_t storedChecksum;
uint8_t calculatedBoxChecksum;
if(boxIndex_ == currentBoxIndex)
{
return true;
}
const uint8_t bankIndex = getGen1BoxBankIndex(boxIndex_, currentBoxIndex);
const uint16_t offset = 0x1A4D + (boxIndex_ % 6);
saveManager_.seekToBankOffset(bankIndex, offset);
saveManager_.readByte(storedChecksum);
calculatedBoxChecksum = calculateBoxChecksum(saveManager_, boxIndex_, currentBoxIndex);
return (storedChecksum == calculatedBoxChecksum);
}
void Gen1Box::updateChecksum(uint8_t currentBoxIndex)
{
if(boxIndex_ == currentBoxIndex)
{
return;
}
const uint8_t calculatedBoxChecksum = calculateBoxChecksum(saveManager_, boxIndex_, currentBoxIndex);
const uint8_t bankIndex = getGen1BoxBankIndex(boxIndex_, currentBoxIndex);
const uint16_t offset = 0x1A4D + (boxIndex_ % 6);
saveManager_.seekToBankOffset(bankIndex, offset);
saveManager_.writeByte(calculatedBoxChecksum);
}

View File

@ -0,0 +1,468 @@
#include "gen1/Gen1SpriteDecoder.h"
#include "SpriteRenderer.h"
#include "RomReader.h"
#include <cstring>
#include <cstdio>
// #define DUMP_SPRITEBITPLANES 1
// if you want to compare with something, you can use the chrome devtools on https://rgmechex.com/tech/gen1decompress.html
// to get the resulting bitplane, you can do this command in the console:
// writer.buffer.slice(writer.base, writer.base + 392)
// then you can use this approach to download the arraydata as a file: https://stackoverflow.com/questions/43026521/how-can-i-save-an-array-object-to-file-at-run-time-by-chrome-debug-tools
/**
* @brief This class writes to a buffer in a vertical, column-first manner.
* Supposedly sprites are being written to the buffer vertically during decoding, which is peculiar.
*/
class VerticalBitWriter
{
public:
VerticalBitWriter(uint8_t *buffer, uint8_t spriteWidthInBits, uint8_t spriteHeightInBits)
: buffer_(buffer)
, curByte_(buffer)
, bitIndex_(0)
, spriteWidthInBits_(spriteWidthInBits)
, spriteHeightInBits_(spriteHeightInBits)
, bitsWritten_(0)
, rowIndex_(0)
, columnIndex_(0)
{
}
void writePair(uint8_t pair)
{
writeBit((pair >> 1));
writeBit((pair & 0x1));
}
void writeBit(uint8_t bit)
{
(*curByte_) |= (bit << (7 - bitIndex_));
++bitsWritten_;
advance();
}
uint16_t getBitsWritten() const
{
return bitsWritten_;
}
protected:
void advance()
{
++columnIndex_;
if((columnIndex_ % 2) == 0)
{
++rowIndex_;
if(rowIndex_ == spriteHeightInBits_)
{
rowIndex_ = 0;
}
else
{
columnIndex_ -= 2;
}
}
// storage in column major -> each next byte is the next row
curByte_ = buffer_ + ((columnIndex_ / 8) * spriteHeightInBits_) + rowIndex_;
// printf("bitsWritten: %hu, columnIndex=%hhu, rowIndex=%hhu\n", bitsWritten_, columnIndex_, rowIndex_);
bitIndex_ = (columnIndex_ % 8);
}
private:
uint8_t *buffer_;
uint8_t *curByte_;
uint8_t bitIndex_;
uint8_t spriteWidthInBits_;
uint8_t spriteHeightInBits_;
uint16_t bitsWritten_;
uint8_t rowIndex_;
uint8_t columnIndex_;
};
/**
* @brief based on https://www.youtube.com/watch?v=aF1Yw_wu2cM&t=237s
*/
static uint16_t decodeRLELVIntoN(BitReader &reader)
{
uint16_t l = 0;
uint16_t v = 0;
uint16_t numLBits = 0;
// first read L
do
{
l <<= 1;
l |= reader.read(1);
++numLBits;
} while ((l & 0x1) == 1);
// now read V
for (uint16_t i = 0; i < numLBits; ++i)
{
// first iteration, v = 0, so it would be a NOOP.
// last iteration, we shouldn't shift AFTER doing the read
// so this is why the code is written like this.
v <<= 1;
v |= reader.read(1);
}
return l + v + 1;
}
static void decodeRLE(BitReader &reader, uint16_t outputLengthInBits, uint8_t *outBuf, uint8_t initialPacketType, uint8_t widthInPixels, uint8_t heightInPixels)
{
VerticalBitWriter writer(outBuf, widthInPixels, heightInPixels);
uint8_t bitPair;
// N is the number of 00 pairs to write to the output buffer
uint16_t N;
bool isRLEPacket = (initialPacketType == 0);
while (writer.getBitsWritten() < outputLengthInBits)
{
if (isRLEPacket)
{
// now determine how many 00 pairs to write by decoding LV into N
N = decodeRLELVIntoN(reader);
// printf("\n Writing %hu 00 pairs. bitsWritten=%hu\n", N, writer.getBitsWritten());
for (uint16_t i = 0; i < N; ++i)
{
if(writer.getBitsWritten() < outputLengthInBits)
{
writer.writePair(00);
}
else
{
printf("WARN: can't write 00\n");
}
}
}
else
{
bitPair = reader.read(2);
while(bitPair != 0)
{
// printf("%hhu%hhu ", bitPair >> 1, (bitPair & 0x1));
writer.writePair(bitPair);
if(writer.getBitsWritten() >= outputLengthInBits)
{
break;
}
bitPair = reader.read(2);
}
}
isRLEPacket = !isRLEPacket;
}
// printf("\nend RLE\n");
}
static void decodeDelta(uint8_t *buffer, uint8_t spriteWidthInPixels, uint8_t spriteHeightInPixels)
{
uint8_t lastBit;
uint8_t originalValue;
uint8_t newValue;
uint8_t bitIndex;
uint8_t i;
uint8_t j;
uint16_t bufferOffset;
const uint8_t spriteWidthInBytes = spriteWidthInPixels / 8;
// delta decoding works horizontally. However, the incoming sprite data is stored in a different way:
// the data is stored column first, meaning that contiguous bytes are not horizontal, but vertical.
// Another important detail here is that every byte stores 8 horizontal pixels.
// so that leaves us with some work to calculate the right buffer index in order to work horizontally for delta decoding.
// now why does the order matter? Well the whole algorithm is based on repeating the value of the last bit we encountered
// if a 0 is encountered in the input data (at the right position/location that is), then the last bit will be repeated.
// if a 1 is encountered in the input data, we flip the lastBit
// so the direction matters for the value of the lastBit at any point in time.
for(j=0; j < spriteHeightInPixels; ++j)
{
// at the start of each row, the state is reset to 0
lastBit = 0;
for(i=0; i < spriteWidthInBytes; ++i)
{
bufferOffset = (i * spriteHeightInPixels) + j;
originalValue = (*(buffer + bufferOffset));
newValue = 0;
for(bitIndex=0; bitIndex < 8; ++bitIndex)
{
newValue <<= 1;
// if we encounter a 0 bit, we repeat the lastBit
// if we encounter a 1 bit, we flip the lastBit and output it
if(originalValue & (1 << (7 - bitIndex)))
{
// flip the lastBit
lastBit ^= 0x1;
}
newValue |= lastBit;
}
(*(buffer + bufferOffset)) = newValue;
}
}
}
/**
* @brief With encodingMode 2 or 3, you need to XOR bitplane 0 and bitplane 1 and store the contents into bitplane1
*/
static void xorBitplanes(uint8_t* bitPlane0, uint8_t* bitPlane1)
{
uint16_t i = 0;
while(i < SPRITE_BITPLANE_BUFFER_SIZE_IN_BYTES)
{
bitPlane1[i] ^= bitPlane0[i];
++i;
}
}
/**
* @brief This function does a valign: bottom, halign: center operation on the src buffer content and writes it to the destination buffer
*/
static void centerSprite(uint8_t* src, uint8_t* dst, uint8_t widthInTiles, uint8_t heightInTiles)
{
const uint8_t maxTiles = 7;
const uint8_t verticalOffset = maxTiles - heightInTiles;
const uint8_t horizontalOffset = ((maxTiles - widthInTiles) / 2);
// 8 vertical bytes per tile
const uint16_t spriteHeightInBytes = heightInTiles * 8;
const uint16_t outputStartOffset = (horizontalOffset * maxTiles + verticalOffset) * 8;
uint16_t dstColumnStartOffset;
uint16_t dstCurOffset;
uint16_t x;
uint16_t y;
memset(dst, 0, SPRITE_BITPLANE_BUFFER_SIZE_IN_BYTES);
// copy column by column
// 1 tile horizontally is 1 byte (8 bits per byte -> 1 byte for 8 pixels)
dstColumnStartOffset = outputStartOffset;
for(x=0; x < widthInTiles; ++x)
{
for(y = 0; y < spriteHeightInBytes; ++y)
{
dstCurOffset = dstColumnStartOffset + y;
dst[dstCurOffset] = src[(x * spriteHeightInBytes + y)];
}
dstColumnStartOffset += SPRITE_BUFFER_HEIGHT_IN_PIXELS;
}
}
/**
* @brief This function merges the 2 bitplanes into a single buffer.
* Note that plane1 likely will overlap with destination.
* This is why the function actually works backwards to avoid overwriting content that still needs to be read.
*
* Also: it is expected that starting from plane1, the buffer at least has SPRITE_BUFFER_SIZE_IN_BYTES* 2 bytes available
* In other words: the function relies on the fact that plane0, plane1 and plane2 are all part of the same big buffer
* Because plane1 will be used as if it is SPRITE_BUFFER_SIZE_IN_BYTES * 2 bytes big (thereby occupying the space of plane2 as well)
*
* But we need to talk about the format here. As you may expect, the result is a buffer in which every pixel is 2 bits.
* But what's unexpected is how this is actually realized:
* Instead of putting the relevant bits next to eachother, they actually end up in different bytes.
*
* So byte0 contains the least significant bit of the pixel and byte1 contains the most significant bit of the same pixel
* This is how the gameboy hardware reads it, so this format likely won't be useful as is if you don't intend to display this on a gameboy (emulator?)
*
* Regardless, because of this format, merging the bitplanes is actually quite simple: you just interleave the bytes, not the bits
* source: https://gbdev.io/gb-asm-tutorial/part1/tiles.html#:~:text=The%20concept%20of%20a%20%E2%80%9Ctile,referred%20to%20as%20meta%2Dtiles.
* and: https://rgmechex.com/tech/gen1decompress.html
*/
static void mergeSpriteBitplanes(uint8_t* plane0, uint8_t* plane1, uint8_t* destination)
{
// using an int in the for loop because otherwise the i >=0 condition won't work (because of underflow)
for(int i=SPRITE_BITPLANE_BUFFER_SIZE_IN_BYTES - 1; i >= 0; --i)
{
destination[i * 2 + 1] = plane1[i];
destination[i * 2 + 0] = plane0[i];
}
}
static uint8_t *getSpriteBuffer(uint8_t *bigBuffer, uint8_t index)
{
return bigBuffer + (index * SPRITE_BITPLANE_BUFFER_SIZE_IN_BYTES);
}
static void dumpSpriteBitplane(uint8_t* spriteBuffer, const char* binPath, const char* rawPath, uint8_t spriteWidthInBytes, uint8_t spriteHeightInPixels)
{
#ifdef DUMP_SPRITEBITPLANES
uint16_t bufferOffset;
// convert to PNG like this:
// convert -size 56x56 -depth 8 gray:spriteBuffer1.raw output.png
// by the way, for some reason, imagemagick doesn't properly handle the image if you'd not convert the bitplane to a 8 bit one (and use -depth 8)
FILE* f = fopen(binPath, "w");
fwrite(spriteBuffer, 1, SPRITE_BITPLANE_BUFFER_SIZE_IN_BYTES, f);
fclose(f);
f = fopen(rawPath, "w");
// so weird thing here: because of the way the gameboy works, the data is stored as 8 horizontal pixels per byte,
// but every byte is column major order: so after byte 1, the next byte is the next 8 pixels of the next (vertical) row and so on until we reach the bottom of the sprite
// then the same process continues starting from the top of the next column.
// so in order to construct an image that can be shown by "normal" programs in a left-to-right, top-to-bottom way
// we need to seek within spriteBuffer a lot.
// based on: https://glitchcity.wiki/wiki/Sprite_decompression_(Generation_I)#Bit_order
for(uint8_t j=0; j < spriteHeightInPixels; ++j)
{
for(uint8_t i=0; i < spriteWidthInBytes; ++i)
{
bufferOffset = (i * spriteHeightInPixels) + j;
for(uint8_t bitIndex=0; bitIndex < 8; ++bitIndex)
{
const uint8_t byteVal = (*(spriteBuffer + bufferOffset)) & (1 << (7- bitIndex));
fputc((byteVal) ? 0 : 255, f);
}
}
}
fclose(f);
printf("Dumped sprite bitplane buffer to %s and %s\n", binPath, rawPath);
#else
(void)spriteBuffer;
(void)binPath;
(void)rawPath;
(void)spriteWidthInBytes;
(void)spriteHeightInPixels;
#endif
}
static void dumpDecodedSprite(uint8_t* spriteBuffer, const char* binPath, const char* rawPath)
{
#ifdef DUMP_SPRITEBITPLANES
SpriteRenderer renderer;
const uint32_t rgbBufferSize = renderer.getNumRGBBytesFor(7, 7);
uint8_t* rgbBuffer;
FILE* f = fopen(binPath, "w");
fwrite(spriteBuffer, 1, SPRITE_BITPLANE_BUFFER_SIZE_IN_BYTES * 2, f);
fclose(f);
rgbBuffer = renderer.drawRGB(spriteBuffer, monochromeGBColorPalette, 7, 7);
f = fopen(rawPath, "w");
fwrite(rgbBuffer, 1, rgbBufferSize, f);
fclose(f);
#else
(void)spriteBuffer;
(void)binPath;
(void)rawPath;
#endif
}
Gen1SpriteDecoder::Gen1SpriteDecoder(IRomReader &romReader)
: romReader_(romReader), bigSpriteBuffer_()
{
}
Gen1SpriteDecoder::~Gen1SpriteDecoder()
{
}
uint8_t* Gen1SpriteDecoder::decode(uint8_t bankIndex, uint16_t pointer)
{
if (!romReader_.seekToRomPointer(pointer, bankIndex))
{
return nullptr;
}
memset(bigSpriteBuffer_, 0, SPRITE_BITPLANE_BUFFER_SIZE_IN_BYTES * 3);
BitReader bitReader(romReader_);
const uint8_t widthInTiles = bitReader.read(4);
const uint8_t heightInTiles = bitReader.read(4);
const uint8_t widthInPixels = widthInTiles * 8;
const uint8_t heightInPixels = heightInTiles * 8;
const uint8_t primaryBuffer = bitReader.read(1);
uint8_t initial_packet_type = bitReader.read(1); // first packet is either RLE or data packet
const uint16_t bitPlaneLengthInBits = widthInPixels * heightInPixels;
uint8_t* bufferA = getSpriteBuffer(bigSpriteBuffer_, 0);
uint8_t* bufferB = getSpriteBuffer(bigSpriteBuffer_, 1);
uint8_t* bufferC = getSpriteBuffer(bigSpriteBuffer_, 2);
uint8_t* buffer0;
uint8_t* buffer1;
// read compressed bitplane
if(primaryBuffer)
{
buffer0 = bufferC;
buffer1 = bufferB;
}
else
{
buffer0 = bufferB;
buffer1 = bufferC;
}
decodeRLE(bitReader, bitPlaneLengthInBits, buffer0, initial_packet_type, widthInPixels, heightInPixels);
uint8_t encodingMode = bitReader.read(1);
if(encodingMode == 1)
{
encodingMode = (encodingMode << 1) | bitReader.read(1);
}
else
{
encodingMode = 1;
}
initial_packet_type = bitReader.read(1); // first packet is either RLE or data packet
decodeRLE(bitReader, bitPlaneLengthInBits, buffer1, initial_packet_type, widthInPixels, heightInPixels);
// NOTE: dumpSpriteBitplane does nothing if DUMP_SPRITEBITPLANES is not defined at the top of this source file
dumpSpriteBitplane(buffer0, "spriteBitplane1_RLE.bin", "spriteBitplane1_RLE.raw", SPRITE_BUFFER_WIDTH_IN_PIXELS / 8, SPRITE_BUFFER_HEIGHT_IN_PIXELS);
dumpSpriteBitplane(buffer1, "spriteBitplane2_RLE.bin", "spriteBitplane2_RLE.raw", SPRITE_BUFFER_WIDTH_IN_PIXELS / 8, SPRITE_BUFFER_HEIGHT_IN_PIXELS);
// Okay, phase 1 is done. Now we need to do the operations involving the 2 bitplanes
switch(encodingMode)
{
case 1:
case 3:
decodeDelta(buffer1, widthInPixels, heightInPixels);
decodeDelta(buffer0, widthInPixels, heightInPixels);
break;
case 2:
decodeDelta(buffer0, widthInPixels, heightInPixels);
break;
default:
break;
}
dumpSpriteBitplane(buffer0, "spriteBitplane1_delta.bin", "spriteBitplane1_delta.raw", SPRITE_BUFFER_WIDTH_IN_PIXELS / 8, SPRITE_BUFFER_HEIGHT_IN_PIXELS);
dumpSpriteBitplane(buffer1, "spriteBitplane2_delta.bin", "spriteBitplane2_delta.raw", SPRITE_BUFFER_WIDTH_IN_PIXELS / 8, SPRITE_BUFFER_HEIGHT_IN_PIXELS);
if((encodingMode & 0x2))
{
xorBitplanes(buffer0, buffer1);
}
dumpSpriteBitplane(buffer0, "spriteBitplane1_encmode.bin", "spriteBitplane1_encmode.raw", SPRITE_BUFFER_WIDTH_IN_PIXELS / 8, SPRITE_BUFFER_HEIGHT_IN_PIXELS);
dumpSpriteBitplane(buffer1, "spriteBitplane2_encmode.bin", "spriteBitplane2_encmode.raw", SPRITE_BUFFER_WIDTH_IN_PIXELS / 8, SPRITE_BUFFER_HEIGHT_IN_PIXELS);
centerSprite(bufferB, bufferA, widthInTiles, heightInTiles);
centerSprite(bufferC, bufferB, widthInTiles, heightInTiles);
dumpSpriteBitplane(bufferA, "spriteBitplane1_centered.bin", "spriteBitplane1_centered.raw", SPRITE_BUFFER_WIDTH_IN_PIXELS / 8, SPRITE_BUFFER_HEIGHT_IN_PIXELS);
dumpSpriteBitplane(bufferB, "spriteBitplane2_centered.bin", "spriteBitplane2_centered.raw", SPRITE_BUFFER_WIDTH_IN_PIXELS / 8, SPRITE_BUFFER_HEIGHT_IN_PIXELS);
mergeSpriteBitplanes(bufferA, bufferB, bufferB);
dumpDecodedSprite(bufferB, "sprite.bin", "sprite.rgb");
return bufferB;
}

383
src/gen2/Gen2Common.cpp Normal file
View File

@ -0,0 +1,383 @@
#include "gen2/Gen2Common.h"
#include "gen2/Gen2GameReader.h"
#include "SaveManager.h"
#include "utils.h"
#include "common.h"
#include <cstdlib>
#include <cstring>
#define POKEMON_GOLD_CARTRIDGE_TITLE "POKEMON_GLD"
#define POKEMON_SILVER_CARTRIDGE_TITLE "POKEMON_SLV"
#define POKEMON_CRYSTAL_CARTRIDGE_TITLE "PM_CRYSTAL"
static TextCodePair gen2TextCodes[] = {
{0x56, "……"},
{0x5D, "TRAINER"},
{0x60, ""},
{0x61, ""},
{0x7F, " "},
{0x80, "A"},
{0x81, "B"},
{0x82, "C"},
{0x83, "D"},
{0x84, "E"},
{0x85, "F"},
{0x86, "G"},
{0x87, "H"},
{0x88, "I"},
{0x89, "J"},
{0x8A, "K"},
{0x8B, "L"},
{0x8C, "M"},
{0x8D, "N"},
{0x8E, "O"},
{0x8F, "P"},
{0x90, "Q"},
{0x91, "R"},
{0x92, "S"},
{0x93, "T"},
{0x94, "U"},
{0x95, "V"},
{0x96, "W"},
{0x97, "X"},
{0x98, "Y"},
{0x99, "Z"},
{0x9A, "("},
{0x9B, ")"},
{0x9C, ":"},
{0x9D, ";"},
{0x9E, "["},
{0x9F, "]"},
{0xA0, "a"},
{0xA1, "b"},
{0xA2, "c"},
{0xA3, "d"},
{0xA4, "e"},
{0xA5, "f"},
{0xA6, "g"},
{0xA7, "h"},
{0xA8, "i"},
{0xA9, "j"},
{0xAA, "k"},
{0xAB, "l"},
{0xAC, "m"},
{0xAD, "n"},
{0xAE, "o"},
{0xAF, "p"},
{0xB0, "q"},
{0xB1, "r"},
{0xB2, "s"},
{0xB3, "t"},
{0xB4, "u"},
{0xB5, "v"},
{0xB6, "w"},
{0xB7, "x"},
{0xB8, "y"},
{0xB9, "z"},
{0xBA, "à"},
{0xBB, "è"},
{0xBD, "ù"},
{0xBE, "ß"},
{0xBF, "ç"},
{0xC0, "Ä"},
{0xC1, "Ö"},
{0xC2, "Ü"},
{0xC3, "ä"},
{0xC4, "ö"},
{0xC5, "ü"},
{0xC6, "ë"},
{0xC7, "ï"},
{0xC8, "â"},
{0xC9, "ô"},
{0xCA, "û"},
{0xCB, "ê"},
{0xCC, "î"},
{0xD0, "'d"},
{0xD1, "'l"},
{0xD2, "'m"},
{0xD3, "'r"},
{0xD4, "'s"},
{0xD5, "'t"},
{0xD6, "'v"},
{0xE0, "\'"},
{0xE1, "PK"},
{0xE2, "MN"},
{0xE3, "-"},
{0xE6, "?"},
{0xE7, "!"},
{0xE8, "."},
{0xE9, "&"},
{0xEA, "é"},
{0xEB, "🡆"},
{0xEC, ""},
{0xED, ""},
{0xEE, ""},
{0xEF, ""},
{0xF0, "¥"},
{0xF1, "×"},
{0xF3, "/"},
{0xF4, ","},
{0xF5, ""},
{0xF6, "0"},
{0xF7, "1"},
{0xF8, "2"},
{0xF9, "3"},
{0xFA, "4"},
{0xFB, "5"},
{0xFC, "6"},
{0xFD, "7"},
{0xFE, "8"},
{0xFF, "9"}
};
Gen2ItemList::Gen2ItemList(ISaveManager &saveManager, Gen2ItemListType type, bool isCrystal)
: saveManager_(saveManager)
, type_(type)
, isCrystal_(isCrystal)
{
}
uint8_t Gen2ItemList::getCount()
{
uint8_t result;
if (!seekToBasePos())
{
return 0;
}
if (!saveManager_.readByte(result))
{
result = 0;
}
return result;
}
uint8_t Gen2ItemList::getCapacity()
{
return 20;
}
bool Gen2ItemList::getEntry(uint8_t index, uint8_t &outItemId, uint8_t &outCount)
{
if (!seekToBasePos())
{
return false;
}
if (!saveManager_.advance(1 + (index * 2)))
{
return false;
}
return getNextEntry(outItemId, outCount);
}
bool Gen2ItemList::getNextEntry(uint8_t &outItemId, uint8_t &outCount)
{
const uint8_t peekByte = saveManager_.peek();
// 0xFF is the terminator byte
if(peekByte == 0xFF)
{
return false;
}
saveManager_.readByte(outCount);
saveManager_.readByte(outItemId);
return true;
}
bool Gen2ItemList::add(uint8_t itemId, uint8_t itemCount)
{
uint8_t curItemId;
uint8_t curItemCount;
const uint8_t numItems = getCount();
// search for an existing item entry
// if found, we'll increase the itemcount
// if not found, the internal position of the savemanager will have conveniently moved
// to the position we'd actually need to write the new entry to
while(getNextEntry(curItemId, curItemCount))
{
if(curItemId == itemId)
{
uint8_t newCount = curItemCount + itemCount;
if(newCount > 99)
{
newCount = 99;
}
saveManager_.rewind();
saveManager_.writeByte(newCount);
return true;
}
}
// no existing entry found in the itemlist
// first check if there's any room left for the new item
if(numItems >= getCapacity())
{
return false;
}
// in the while loop, getNextEntry has returned false on the last iteration, so that means
// we've arrived at the terminator of the list. This is the exact position at which we need to write our new entry.
// convenient, isn't it?
// so let's write the new entry...
saveManager_.writeByte(itemCount);
saveManager_.writeByte(itemId);
// now write the new terminator
saveManager_.writeByte(0xFF);
// the only thing left is to increase the list counter
seekToBasePos();
saveManager_.writeByte(numItems + 1);
return true;
}
bool Gen2ItemList::seekToBasePos()
{
uint32_t offset;
switch (type_)
{
case GEN2_ITEMLISTTYPE_ITEMPOCKET:
offset = (isCrystal_) ? 0x2420 : 0x241F;
break;
case GEN2_ITEMLISTTYPE_KEYITEMPOCKET:
offset = (isCrystal_) ? 0x244A : 0x2449;
break;
case GEN2_ITEMLISTTYPE_BALLPOCKET:
offset = (isCrystal_) ? 0x2465 : 0x2464;
break;
case GEN2_ITEMLISTTYPE_PC:
offset = (isCrystal_) ? 0x247F : 0x247E;
break;
default:
return false;
}
return saveManager_.seek(offset);
}
Gen2Checksum::Gen2Checksum()
: checksum_(0)
{
}
void Gen2Checksum::addByte(uint8_t byte)
{
checksum_ += byte;
}
uint16_t Gen2Checksum::get() const
{
return checksum_;
}
Gen2GameType gen2_determineGameType(const GameboyCartridgeHeader& cartridgeHeader)
{
Gen2GameType result;
if (strncmp(cartridgeHeader.title, POKEMON_GOLD_CARTRIDGE_TITLE, sizeof(POKEMON_GOLD_CARTRIDGE_TITLE) - 1) == 0)
{
result = Gen2GameType::GOLD;
}
else if (strncmp(cartridgeHeader.title, POKEMON_SILVER_CARTRIDGE_TITLE, sizeof(POKEMON_SILVER_CARTRIDGE_TITLE) - 1) == 0)
{
result = Gen2GameType::SILVER;
}
else if (strncmp(cartridgeHeader.title, POKEMON_CRYSTAL_CARTRIDGE_TITLE, sizeof(POKEMON_CRYSTAL_CARTRIDGE_TITLE) - 1) == 0)
{
result = Gen2GameType::CRYSTAL;
}
else
{
result = Gen2GameType::INVALID;
}
return result;
}
void gen2_recalculatePokeStats(Gen2GameReader &reader, Gen2TrainerPokemon &poke)
{
Gen2PokeStats stats;
reader.readPokemonStatsForIndex(poke.poke_index, stats);
poke.max_hp = calculatePokeStat(PokeStat::HP, stats.base_hp, getStatIV(PokeStat::HP, poke.iv_data), poke.hp_effort_value, poke.level);
poke.atk = calculatePokeStat(PokeStat::ATK, stats.base_attack, getStatIV(PokeStat::ATK, poke.iv_data), poke.atk_effort_value, poke.level);
poke.def = calculatePokeStat(PokeStat::DEF, stats.base_defense, getStatIV(PokeStat::DEF, poke.iv_data), poke.def_effort_value, poke.level);
poke.speed = calculatePokeStat(PokeStat::SPEED, stats.base_speed, getStatIV(PokeStat::SPEED, poke.iv_data), poke.speed_effort_value, poke.level);
poke.special_atk = calculatePokeStat(PokeStat::SPECIAL_ATK, stats.base_special_attack, getStatIV(PokeStat::SPECIAL_ATK, poke.iv_data), poke.special_effort_value, poke.level);
poke.special_def = calculatePokeStat(PokeStat::SPECIAL_DEF, stats.base_special_defense, getStatIV(PokeStat::SPECIAL_DEF, poke.iv_data), poke.special_effort_value, poke.level);
}
uint16_t gen2_decodePokeText(const uint8_t *inputBuffer, uint16_t inputBufferLength, char *outputBuffer, uint16_t outputBufferLength)
{
const uint16_t numEntries = sizeof(gen2TextCodes) / sizeof(struct TextCodePair);
return decodeText(gen2TextCodes, numEntries, inputBuffer, inputBufferLength, outputBuffer, outputBufferLength);
}
uint16_t gen2_encodePokeText(const char *inputBuffer, uint16_t inputBufferLength, uint8_t *outputBuffer, uint16_t outputBufferLength, uint8_t terminator)
{
const uint16_t numEntries = sizeof(gen2TextCodes) / sizeof(struct TextCodePair);
return encodeText(gen2TextCodes, numEntries, inputBuffer, inputBufferLength, outputBuffer, outputBufferLength, terminator);
}
bool gen2_isPokemonShiny(Gen2TrainerPokemon& poke)
{
// based on https://bulbapedia.bulbagarden.net/wiki/Shiny_Pok%C3%A9mon#Determining_Shininess
const PokeStat statsToCheckFor10[] = {PokeStat::SPEED, PokeStat::DEF, PokeStat::SPECIAL};
for(uint8_t i=0; i < sizeof(statsToCheckFor10); ++i)
{
if(getStatIV(statsToCheckFor10[i], poke.iv_data) != 10)
{
return false;
}
}
switch(getStatIV(PokeStat::ATK, poke.iv_data))
{
case 2:
case 3:
case 6:
case 7:
case 10:
case 11:
case 14:
case 15:
return true;
default:
break;
}
return false;
}
void gen2_makePokemonShiny(Gen2TrainerPokemon& poke)
{
const uint8_t validAtkValues[] = { 2, 3, 6, 7, 10, 11, 14, 15 };
uint8_t i;
bool atkValid = false;
uint8_t atkIV = getStatIV(PokeStat::ATK, poke.iv_data);
for(i=0; i < sizeof(validAtkValues); ++i)
{
if(atkIV == validAtkValues[i])
{
atkValid = true;
break;
}
}
if(!atkValid)
{
i = rand() % sizeof(validAtkValues);
atkIV = validAtkValues[i];
}
// the first 4 bits need to be the valid atk IV (one of the validAtkValues list values)
// all the other groups of 4 bits need to be set to 10 (SPEED, DEFENSE, SPECIAL)
poke.iv_data[0] = (atkIV << 4) | 0xA;
poke.iv_data[1] = 0xAA;
}

File diff suppressed because it is too large Load Diff

691
src/gen2/Gen2GameReader.cpp Normal file
View File

@ -0,0 +1,691 @@
#include "gen2/Gen2GameReader.h"
#include "RomReader.h"
#include "SaveManager.h"
#include "utils.h"
#include <cstdlib>
// needed for crystal_fixPicBank() below
#define CRYSTAL_PICS_FIX 0x36
#define CRYSTAL_BANK_PICS_1 72
static const uint8_t crystalPicsBanks[] = {
CRYSTAL_BANK_PICS_1 + 0,
CRYSTAL_BANK_PICS_1 + 1,
CRYSTAL_BANK_PICS_1 + 2,
CRYSTAL_BANK_PICS_1 + 3,
CRYSTAL_BANK_PICS_1 + 4,
CRYSTAL_BANK_PICS_1 + 5,
CRYSTAL_BANK_PICS_1 + 6,
CRYSTAL_BANK_PICS_1 + 7,
CRYSTAL_BANK_PICS_1 + 8,
CRYSTAL_BANK_PICS_1 + 9,
CRYSTAL_BANK_PICS_1 + 10,
CRYSTAL_BANK_PICS_1 + 11,
CRYSTAL_BANK_PICS_1 + 12,
CRYSTAL_BANK_PICS_1 + 13,
CRYSTAL_BANK_PICS_1 + 14,
CRYSTAL_BANK_PICS_1 + 15,
CRYSTAL_BANK_PICS_1 + 16,
CRYSTAL_BANK_PICS_1 + 17,
CRYSTAL_BANK_PICS_1 + 18,
CRYSTAL_BANK_PICS_1 + 19,
CRYSTAL_BANK_PICS_1 + 20,
CRYSTAL_BANK_PICS_1 + 21,
CRYSTAL_BANK_PICS_1 + 22,
CRYSTAL_BANK_PICS_1 + 23
};
/**
* @brief This function was created based on engine/gfx/load_pics.asm in https://github.com/pret/pokecrystal/tree/master (FixPicBank)
* For some reason the bankByte is being "fixed" in Pokémon Crystal
*
* ChatGPT assisted me with this function, because I'm not great at interpreting assembly.
*/
static uint8_t crystal_fixPicBank(uint8_t bankIndex)
{
// Calculate the offset in the PicsBanks array
const int16_t offset = static_cast<int16_t>(bankIndex) - (CRYSTAL_BANK_PICS_1 - CRYSTAL_PICS_FIX);
// Ensure the offset is within the bounds of the PicsBanks array
if (offset < 0 || offset >= static_cast<int16_t>(sizeof(crystalPicsBanks)))
{
// Handle error: invalid offset
return 0; // or some error code
}
// Return the bank number from the PicsBanks array
return crystalPicsBanks[offset];
}
/**
* @brief Based on docs/gen2.txt line 104
*/
static uint8_t gold_silver_fixPicBank(uint8_t bankIndex)
{
uint8_t result;
// remap bankbyte according to docs/gen2.txt
switch (bankIndex)
{
case 0x13:
result = 0x1F;
break;
case 0x14:
result = 0x20;
break;
case 0x1F:
result = 0x2E;
break;
default:
result = bankIndex;
break;
}
return result;
}
static void addBytesToChecksum(ISaveManager &saveManager, uint16_t savOffsetStart, uint16_t savOffsetEnd, Gen2Checksum &checksum)
{
if (!saveManager.seek(savOffsetStart))
{
return;
}
uint8_t byte;
for (uint16_t i = savOffsetStart; i < savOffsetEnd + 1; ++i)
{
saveManager.readByte(byte);
checksum.addByte(byte);
}
}
// based on https://bulbapedia.bulbagarden.net/wiki/Save_data_structure_(Generation_II)#Gold_and_Silver
static uint16_t calculateMainRegionChecksum(ISaveManager &saveManager, bool isGameCrystal)
{
Gen2Checksum checksum;
const uint16_t savOffsetStart = 0x2009;
const uint16_t savOffsetEnd = (isGameCrystal) ? 0x2B82 : 0x2D68;
addBytesToChecksum(saveManager, savOffsetStart, savOffsetEnd, checksum);
return checksum.get();
}
// based on https://bulbapedia.bulbagarden.net/wiki/Save_data_structure_(Generation_II)#Gold_and_Silver
static uint16_t calculateBackupRegionChecksum(ISaveManager &saveManager, bool isGameCrystal)
{
Gen2Checksum checksum;
if (isGameCrystal)
{
// for crystal, the backup save region is a contiguous address range
addBytesToChecksum(saveManager, 0x1209, 0x1D82, checksum);
}
else
{
// for Gold/Silver the backup save region is split up into 5 blocks
addBytesToChecksum(saveManager, 0x15C7, 0x17EC, checksum); // 550 bytes
addBytesToChecksum(saveManager, 0x3D96, 0x3F3F, checksum); // 426 bytes
addBytesToChecksum(saveManager, 0x0C6B, 0x10E7, checksum); // 1149 bytes
addBytesToChecksum(saveManager, 0x7E39, 0x7E6C, checksum); // 52 bytes
addBytesToChecksum(saveManager, 0x10E8, 0x15C6, checksum); // 1247 bytes
}
return checksum.get();
}
static uint16_t readMainChecksum(ISaveManager &saveManager, bool isGameCrystal)
{
uint16_t result;
if (isGameCrystal)
{
saveManager.seek(0x2D0D);
saveManager.readUint16(result, Endianness::LITTLE);
}
else
{
saveManager.seek(0x2D69);
saveManager.readUint16(result, Endianness::LITTLE);
}
return result;
}
static uint16_t readBackupChecksum(ISaveManager &saveManager, bool isGameCrystal)
{
uint16_t result;
if (isGameCrystal)
{
saveManager.seek(0x1F0D);
saveManager.readUint16(result, Endianness::LITTLE);
}
else
{
saveManager.seek(0x7E6D);
saveManager.readUint16(result, Endianness::LITTLE);
}
return result;
}
static void writeMainChecksum(ISaveManager &saveManager, uint16_t checksum, bool isGameCrystal)
{
if (isGameCrystal)
{
saveManager.seek(0x2D0D);
saveManager.writeUint16(checksum, Endianness::LITTLE);
}
else
{
saveManager.seek(0x2D69);
saveManager.writeUint16(checksum, Endianness::LITTLE);
}
}
static void writeBackupChecksum(ISaveManager &saveManager, uint16_t checksum, bool isGameCrystal)
{
if (isGameCrystal)
{
saveManager.seek(0x1F0D);
saveManager.writeUint16(checksum, Endianness::LITTLE);
}
else
{
saveManager.seek(0x7E6D);
saveManager.writeUint16(checksum, Endianness::LITTLE);
}
}
static void updateMainChecksum(ISaveManager &saveManager, bool isCrystal)
{
const uint16_t calculatedChecksum = calculateMainRegionChecksum(saveManager, isCrystal);
writeMainChecksum(saveManager, calculatedChecksum, isCrystal);
}
Gen2GameReader::Gen2GameReader(IRomReader &romReader, ISaveManager &saveManager, Gen2GameType gameType)
: romReader_(romReader), saveManager_(saveManager), spriteDecoder_(romReader), gameType_(gameType)
{
}
Gen2GameReader::~Gen2GameReader()
{
}
const char *Gen2GameReader::getPokemonName(uint8_t index) const
{
static char result[20];
uint8_t encodedText[0xA];
uint32_t numRead;
const uint32_t romOffset = (isGameCrystal()) ? 0x53384 : 0x1B0B74;
romReader_.seek(romOffset + (0xA * (index - 1)));
numRead = romReader_.readUntil(encodedText, 0x50, 0xA);
gen2_decodePokeText(encodedText, numRead, result, sizeof(result) - 1);
return result;
}
bool Gen2GameReader::isValidIndex(uint8_t index) const
{
return (index != 0) && (index < 252);
}
bool Gen2GameReader::readPokemonStatsForIndex(uint8_t index, Gen2PokeStats &outStats) const
{
const uint8_t statsStructSize = 32;
const uint32_t romOffset = (isGameCrystal()) ? 0x051424 : 0x51B0B;
romReader_.seek(romOffset);
// now move to the specific pokemon stats entry
// this is 1-based
if (!romReader_.advance(statsStructSize * (index - 1)))
{
return false;
}
romReader_.readByte(outStats.pokedex_number);
romReader_.readByte(outStats.base_hp);
romReader_.readByte(outStats.base_attack);
romReader_.readByte(outStats.base_defense);
romReader_.readByte(outStats.base_speed);
romReader_.readByte(outStats.base_special_attack);
romReader_.readByte(outStats.base_special_defense);
romReader_.readByte(outStats.type1);
romReader_.readByte(outStats.type2);
romReader_.readByte(outStats.catch_rate);
romReader_.readByte(outStats.base_exp_yield);
romReader_.readByte(outStats.wild_held_item1);
romReader_.readByte(outStats.wild_held_item2);
romReader_.readByte(outStats.gender_ratio);
romReader_.readByte(outStats.unknown);
romReader_.readByte(outStats.egg_cycles);
romReader_.readByte(outStats.unknown2);
romReader_.readByte(outStats.front_sprite_dimensions);
romReader_.read(outStats.blank, 4);
romReader_.readByte(outStats.growth_rate);
romReader_.readByte(outStats.egg_groups);
romReader_.read(outStats.tm_hm_flags, 8);
return true;
}
bool Gen2GameReader::readFrontSpritePointer(uint8_t index, uint8_t &outBankIndex, uint16_t &outPointer)
{
const uint8_t bytesPerPokemon = 6;
// For Crystal, this offset was really hard to find.
// I eventually found it with these 2 files:
// https://raw.githubusercontent.com/pret/pokecrystal/symbols/pokecrystal.map
// https://github.com/pret/pokecrystal/blob/master/data/pokemon/pic_pointers.asm
//
const uint32_t romOffset = (isGameCrystal()) ? 0x120000 : 0x48000;
// I don't support Unown sprite decoding right now, because unown is stored separately based on the variant.
if (index == 201)
{
return false;
}
if (!romReader_.seek(romOffset))
{
return false;
}
if (!romReader_.advance((index - 1) * bytesPerPokemon))
{
return false;
}
romReader_.readByte(outBankIndex);
// for some reason, the bank byte doesn't contain the actual bank index
// it's a remapped version of it. So we need to convert it.
if (isGameCrystal())
{
outBankIndex = crystal_fixPicBank(outBankIndex);
}
else
{
outBankIndex = gold_silver_fixPicBank(outBankIndex);
}
// NOTE: the pointer returned is a pointer to the memory address once the bank is mapped.
// what you need to know here is that bank 0 is always mapped in a gameboy
// but the memory addresses directly after those of bank 0 are the memory addresses at which the switchable bank is mapped to.
// Therefore, once mapped, an offset within the switchable bank will ALWAYS be mapped to an address between 0x4000 and 0x8000
// This is why the read pointer will also always be a value between 0x4000 and 0x8000, as it is a pointer within the mapped address space
// to convert to an actual offset within the relevant bank, we just need to subtract 0x4000 from this pointer
// We deal with this in our IRomReader implementation. See seekToRomPointer()
romReader_.readUint16(outPointer, Endianness::LITTLE);
return true;
}
void Gen2GameReader::readSpriteDimensions(const Gen2PokeStats &poke, uint8_t &outWidthInTiles, uint8_t &outHeightInTiles)
{
outWidthInTiles = poke.front_sprite_dimensions >> 4;
outHeightInTiles = poke.front_sprite_dimensions & 0xF;
}
bool Gen2GameReader::readColorPaletteForPokemon(uint8_t index, bool shiny, uint16_t *outColorPalette)
{
// index is 1-based.
// 2 color palettes are offered per pokemon: non-shiny (4 bytes), shiny (4 bytes)
// each stored color palette only has 2 out of 4 colors actually stored in the rom.
// that's because for every color palette, the first color is white and the last color is black.
// so the rom only stores the 2nd and 3rd colors
// each of these colors is an uint16_t
const uint32_t romOffset = (isGameCrystal()) ? 0xA8D6 : 0xAD45;
if (!romReader_.seek(romOffset + ((index - 1) * 8)))
{
return false;
}
if (shiny)
{
if (!romReader_.advance(4))
{
return false;
}
}
uint16_t *curColor = outColorPalette;
// first color is always white
(*curColor) = 0x7FFF;
++curColor;
// only the second and third color are actually stored in gen 2
romReader_.read((uint8_t *)curColor, 2);
++curColor;
romReader_.read((uint8_t *)curColor, 2);
++curColor;
// last color is always black
(*curColor) = 0;
return true;
}
uint8_t *Gen2GameReader::decodeSprite(uint8_t bankIndex, uint16_t pointer)
{
return spriteDecoder_.decode(bankIndex, pointer);
}
uint8_t Gen2GameReader::addPokemon(Gen2TrainerPokemon &poke, bool isEgg, const char *originalTrainerID, const char *nickname)
{
Gen2Party party = getParty();
uint8_t result = 0xFF;
if (!originalTrainerID)
{
originalTrainerID = getTrainerName();
}
if (party.getNumberOfPokemon() < party.getMaxNumberOfPokemon())
{
party.add(poke, isEgg, originalTrainerID, nickname);
result = 0xFE;
}
else
{
for (uint8_t i = 0; i < 14; ++i)
{
Gen2Box box = getBox(i);
if (box.getNumberOfPokemon() == box.getMaxNumberOfPokemon())
{
continue;
}
box.add(poke, isEgg, originalTrainerID, nickname);
result = i;
break;
}
}
setPokedexFlag(PokedexFlag::POKEDEX_SEEN, poke.poke_index);
setPokedexFlag(PokedexFlag::POKEDEX_OWNED, poke.poke_index);
return result;
}
uint8_t Gen2GameReader::addDistributionPokemon(const Gen2DistributionPokemon &distributionPoke, const char *nickname)
{
Gen2TrainerPokemon poke = distributionPoke.poke;
const char *originalTrainerName;
if (distributionPoke.setPlayerAsOriginalTrainer)
{
originalTrainerName = getTrainerName();
poke.original_trainer_ID = getTrainerID();
}
else
{
originalTrainerName = distributionPoke.originalTrainer;
if (distributionPoke.regenerateTrainerID)
{
if (distributionPoke.originalTrainerID)
{
// limit set, apply it
poke.original_trainer_ID = (uint16_t)(rand() % distributionPoke.originalTrainerID);
}
else
{
// no limit. The max is the max of the uint16_t type
poke.original_trainer_ID = (uint16_t)(rand() % UINT16_MAX);
}
}
else
{
poke.original_trainer_ID = distributionPoke.originalTrainerID;
}
}
if (distributionPoke.shinyChance != 0xFF && (rand() % 100) <= distributionPoke.shinyChance)
{
// the pokemon will be shiny
gen2_makePokemonShiny(poke);
}
else if (distributionPoke.randomizeIVs)
{
const uint16_t randomVal = (uint16_t)rand();
poke.iv_data[0] = (uint8_t)(randomVal >> 8);
poke.iv_data[1] = (uint8_t)(randomVal & 0xFF);
}
else
{
poke.iv_data[0] = distributionPoke.iv_data[0];
poke.iv_data[1] = distributionPoke.iv_data[1];
}
return addPokemon(poke, distributionPoke.isEgg, originalTrainerName, nickname);
}
uint16_t Gen2GameReader::getTrainerID() const
{
uint16_t result = 0;
if (!saveManager_.seek(0x2009))
{
return result;
}
saveManager_.readUint16(result, Endianness::BIG);
return result;
}
const char *Gen2GameReader::getTrainerName() const
{
static char result[20];
uint8_t encodedPlayerName[0xB];
saveManager_.seek(0x200B);
saveManager_.readUntil(encodedPlayerName, 0x50, 0xB);
gen2_decodePokeText(encodedPlayerName, sizeof(encodedPlayerName), result, sizeof(result));
return result;
}
const char *Gen2GameReader::getRivalName() const
{
static char result[20];
uint8_t encodedPlayerName[0xB];
saveManager_.seek(0x2021);
saveManager_.readUntil(encodedPlayerName, 0x50, 0xB);
gen2_decodePokeText(encodedPlayerName, sizeof(encodedPlayerName), result, sizeof(result));
return result;
}
uint8_t Gen2GameReader::getCurrentBoxIndex()
{
uint8_t result;
const uint16_t saveOffset = (isGameCrystal()) ? 0x2700 : 0x2724;
if (!saveManager_.seek(saveOffset))
{
return 0;
}
saveManager_.readByte(result);
// only the lowest 4 bits of this value are the box index
return (result & 0xF);
}
Gen2Party Gen2GameReader::getParty()
{
return Gen2Party((*this), saveManager_);
}
Gen2Box Gen2GameReader::getBox(uint8_t boxIndex)
{
return Gen2Box((*this), saveManager_, boxIndex);
}
Gen2ItemList Gen2GameReader::getItemList(Gen2ItemListType type)
{
return Gen2ItemList(saveManager_, type, isGameCrystal());
}
bool Gen2GameReader::getPokedexFlag(PokedexFlag dexFlag, uint8_t pokedexNumber)
{
uint16_t saveOffset;
if (isGameCrystal())
{
saveOffset = (dexFlag == POKEDEX_SEEN) ? 0x2A47 : 0x2A27;
}
else
{
saveOffset = (dexFlag == POKEDEX_SEEN) ? 0x2A6C : 0x2A4C;
}
uint8_t byte;
if (pokedexNumber < 1 || pokedexNumber > 251)
{
return false;
}
// for the next operations, the pokedexNumber will be used as a zero-based value
--pokedexNumber;
saveManager_.seek(saveOffset + (pokedexNumber / 8));
saveManager_.readByte(byte);
// in this case, bits are ordered within bytes from lowest to highest
// source: https://bulbapedia.bulbagarden.net/wiki/Save_data_structure_(Generation_I)#bank1_main_pokedex
return (byte >> (pokedexNumber % 8)) & 0x1;
}
void Gen2GameReader::setPokedexFlag(PokedexFlag dexFlag, uint8_t pokedexNumber)
{
uint16_t saveOffset;
if (isGameCrystal())
{
saveOffset = (dexFlag == POKEDEX_SEEN) ? 0x2A47 : 0x2A27;
}
else
{
saveOffset = (dexFlag == POKEDEX_SEEN) ? 0x2A6C : 0x2A4C;
}
uint8_t byte;
if (pokedexNumber < 1 || pokedexNumber > 251)
{
return;
}
// for the next operations, the pokedexNumber will be used as a zero-based value
--pokedexNumber;
saveManager_.seek(saveOffset + (pokedexNumber / 8));
byte = saveManager_.peek();
// in this case, bits are ordered within bytes from lowest to highest
// source: https://bulbapedia.bulbagarden.net/wiki/Save_data_structure_(Generation_I)#bank1_main_pokedex
byte |= (1 << (pokedexNumber % 8));
saveManager_.writeByte(byte);
}
uint8_t Gen2GameReader::getPokedexCounter(PokedexFlag dexFlag)
{
uint16_t saveOffset;
if (isGameCrystal())
{
saveOffset = (dexFlag == POKEDEX_SEEN) ? 0x2A47 : 0x2A27;
}
else
{
saveOffset = (dexFlag == POKEDEX_SEEN) ? 0x2A6C : 0x2A4C;
}
uint8_t bytes[32];
uint8_t result = 0;
saveManager_.seek(saveOffset);
saveManager_.read(bytes, sizeof(bytes));
const uint8_t *cur = bytes;
const uint8_t *const end = bytes + sizeof(bytes);
uint8_t bit = 0;
while (cur < end)
{
bit = 0;
// while we're not at the last bit yet, walk through all the bits and add to counter if set
while (bit != 0x80)
{
// this if may look a bit strange here, but this was an easy way to evaluate the first bit
// without having to duplicate the bit check below
if (bit == 0)
{
bit = 1;
}
else
{
bit <<= 1;
}
if ((*cur) & bit)
{
++result;
}
}
++cur;
}
return result;
}
bool Gen2GameReader::isMainChecksumValid()
{
const bool isCrystal = isGameCrystal();
const uint16_t storedChecksum = readMainChecksum(saveManager_, isCrystal);
const uint16_t calculatedChecksum = calculateMainRegionChecksum(saveManager_, isCrystal);
return (storedChecksum == calculatedChecksum);
}
bool Gen2GameReader::isBackupChecksumValid()
{
const bool isCrystal = isGameCrystal();
const uint16_t storedChecksum = readBackupChecksum(saveManager_, isCrystal);
const uint16_t calculatedChecksum = calculateBackupRegionChecksum(saveManager_, isCrystal);
return (storedChecksum == calculatedChecksum);
}
void Gen2GameReader::finishSave()
{
const bool isCrystal = isGameCrystal();
const uint16_t currentPcBoxSaveOffset = (isCrystal) ? 0x2D10 : 0x2D6C;
Gen2Box currentBox = getBox(getCurrentBoxIndex());
#if 1
// we may have modified or current PC box. However, Gen2GameReader doesn't modify this section in the save directly/immediately.
// instead we write directly to the intended PC box area when we're doign changes.
// and we rely on this function (finishSave) to copy the changed PC box to the current PC box save region.
saveManager_.copyRegion(currentBox.getSaveOffset(), currentPcBoxSaveOffset, GEN2_PC_BOX_SIZE_IN_BYTES);
#endif
updateMainChecksum(saveManager_, isCrystal);
// now start copying the main save region to the backup save region(s)
if (isCrystal)
{
// for crystal, the backup save region is a contiguous address range
saveManager_.copyRegion(0x2009, 0x1209, 2938);
}
else
{
// for Gold/Silver the backup save region is split up into 3 blocks
saveManager_.copyRegion(0x2009, 0x15C7, 550); // 550 bytes
saveManager_.copyRegion(0x222F, 0x3D96, 426); // 426 bytes
saveManager_.copyRegion(0x23D9, 0x0C6B, 1149); // 1149 bytes
saveManager_.copyRegion(0x2856, 0x7E39, 52); // 52 bytes
saveManager_.copyRegion(0x288A, 0x10E8, 1247); // 1247 bytes
}
// read main data checksum and copy that to the backup checksum
writeBackupChecksum(saveManager_, readMainChecksum(saveManager_, isCrystal), isCrystal);
}
bool Gen2GameReader::isGameCrystal() const
{
return (gameType_ == Gen2GameType::CRYSTAL);
}

View File

@ -0,0 +1,420 @@
#include "gen2/Gen2PlayerPokemonStorage.h"
#include "gen2/Gen2GameReader.h"
#include "SaveManager.h"
#include <cstring>
static const int ORIGINAL_TRAINER_NAME_SIZE = 0xB;
static const int NICKNAME_SIZE = 0xB;
static const uint8_t PARTY_LIST_CAPACITY = 6;
static const uint8_t PARTY_LIST_PKMN_ENTRY_SIZE = 48;
static const uint8_t PC_BOX_LIST_CAPACITY = 20;
static const uint8_t PC_BOX_LIST_PKMN_ENTRY_SIZE = 32;
Gen2PokemonList::Gen2PokemonList(Gen2GameReader& gameReader, ISaveManager& saveManager, uint8_t listCapacity, uint8_t entrySize)
: gameReader_(gameReader)
, saveManager_(saveManager)
, listCapacity_(listCapacity)
, entrySize_(entrySize)
{
}
Gen2PokemonList::~Gen2PokemonList()
{
}
uint8_t Gen2PokemonList::getNumberOfPokemon()
{
uint8_t result;
if(!saveManager_.seek(getSaveOffset()))
{
return 0;
}
saveManager_.readByte(result);
return result;
}
uint8_t Gen2PokemonList::getMaxNumberOfPokemon() const
{
return listCapacity_;
}
bool Gen2PokemonList::setNumberOfPokemon(uint8_t count)
{
if(!saveManager_.seek(getSaveOffset()))
{
return false;
}
saveManager_.writeByte(count);
return true;
}
uint8_t Gen2PokemonList::getSpeciesAtIndex(uint8_t index)
{
if(!saveManager_.seek(getSaveOffset() + 1 + index))
{
return 0;
}
return getNextSpecies();
}
bool Gen2PokemonList::setSpeciesAtIndex(uint8_t index, uint8_t speciesIndex)
{
if(!saveManager_.seek(getSaveOffset() + 1 + index))
{
return false;
}
saveManager_.writeByte(speciesIndex);
return true;
}
uint8_t Gen2PokemonList::getNextSpecies()
{
uint8_t result;
saveManager_.readByte(result);
return result;
}
bool Gen2PokemonList::getPokemon(uint8_t index, Gen2TrainerPokemon& outPoke)
{
if(!saveManager_.seek(getSaveOffset() + listCapacity_ + 2 + (index * entrySize_)))
{
return false;
}
return getNextPokemon(outPoke);
}
bool Gen2PokemonList::isEgg(uint8_t index)
{
return (getSpeciesAtIndex(index) == 0xFD);
}
bool Gen2PokemonList::getNextPokemon(Gen2TrainerPokemon& outPoke)
{
saveManager_.readByte(outPoke.poke_index);
saveManager_.readByte(outPoke.held_item_index);
saveManager_.readByte(outPoke.index_move1);
saveManager_.readByte(outPoke.index_move2);
saveManager_.readByte(outPoke.index_move3);
saveManager_.readByte(outPoke.index_move4);
saveManager_.readUint16(outPoke.original_trainer_ID, Endianness::BIG);
saveManager_.readUint24(outPoke.exp, Endianness::BIG);
saveManager_.readUint16(outPoke.hp_effort_value, Endianness::BIG);
saveManager_.readUint16(outPoke.atk_effort_value, Endianness::BIG);
saveManager_.readUint16(outPoke.def_effort_value, Endianness::BIG);
saveManager_.readUint16(outPoke.speed_effort_value, Endianness::BIG);
saveManager_.readUint16(outPoke.special_effort_value, Endianness::BIG);
// no endianness stuff for IV. Make sure to keep as is
saveManager_.read(outPoke.iv_data, 2);
saveManager_.readByte(outPoke.pp_move1);
saveManager_.readByte(outPoke.pp_move2);
saveManager_.readByte(outPoke.pp_move3);
saveManager_.readByte(outPoke.pp_move4);
saveManager_.readByte(outPoke.friendship_or_remaining_egg_cycles);
saveManager_.readByte(outPoke.pokerus);
saveManager_.readUint16(outPoke.caught_data, Endianness::BIG);
saveManager_.readByte(outPoke.level);
if(entrySize_ == 48)
{
// party pokemon -> read "temporary" field values
saveManager_.readByte(outPoke.status_condition);
saveManager_.readByte(outPoke.unused_byte);
saveManager_.readUint16(outPoke.current_hp, Endianness::BIG);
saveManager_.readUint16(outPoke.max_hp, Endianness::BIG);
saveManager_.readUint16(outPoke.atk, Endianness::BIG);
saveManager_.readUint16(outPoke.def, Endianness::BIG);
saveManager_.readUint16(outPoke.speed, Endianness::BIG);
saveManager_.readUint16(outPoke.special_atk, Endianness::BIG);
saveManager_.readUint16(outPoke.special_def, Endianness::BIG);
}
else
{
// box pokemon -> recalculate "temporary" field values
outPoke.status_condition = 0;
outPoke.unused_byte = 0;
gen2_recalculatePokeStats(gameReader_, outPoke);
}
return false;
}
bool Gen2PokemonList::setPokemon(uint8_t index, const Gen2TrainerPokemon& orig, bool isEgg)
{
Gen2TrainerPokemon poke;
Gen2PokeStats stats;
uint8_t remainingEggCycles;
// work on a copy of the incoming pokemon
// this is needed to keep it const on passing, but modify it during writing (for the temp stats)
memcpy(&poke, &orig, sizeof(Gen2TrainerPokemon));
// if the pokemon is supposed to be an egg, the species index needs to be set as 0xFD
if(!setSpeciesAtIndex(index, (isEgg) ? 0xFD : poke.poke_index))
{
return false;
}
if(!saveManager_.seek(getSaveOffset() + listCapacity_ + 2 + (index * entrySize_)))
{
return false;
}
if(isEgg)
{
//if the pokemon is supposed to be an egg, the remaining egg cycles field needs to be set accordingly
gameReader_.readPokemonStatsForIndex(poke.poke_index, stats);
remainingEggCycles = stats.egg_cycles;
}
else
{
remainingEggCycles = poke.friendship_or_remaining_egg_cycles;
}
saveManager_.writeByte(poke.poke_index);
saveManager_.writeByte(poke.held_item_index);
saveManager_.writeByte(poke.index_move1);
saveManager_.writeByte(poke.index_move2);
saveManager_.writeByte(poke.index_move3);
saveManager_.writeByte(poke.index_move4);
saveManager_.writeUint16(poke.original_trainer_ID, Endianness::BIG);
saveManager_.writeUint24(poke.exp, Endianness::BIG);
saveManager_.writeUint16(poke.hp_effort_value, Endianness::BIG);
saveManager_.writeUint16(poke.atk_effort_value, Endianness::BIG);
saveManager_.writeUint16(poke.def_effort_value, Endianness::BIG);
saveManager_.writeUint16(poke.speed_effort_value, Endianness::BIG);
saveManager_.writeUint16(poke.special_effort_value, Endianness::BIG);
// no endianness stuff for IV. Make sure to keep as is
saveManager_.write(poke.iv_data, 2);
saveManager_.writeByte(poke.pp_move1);
saveManager_.writeByte(poke.pp_move2);
saveManager_.writeByte(poke.pp_move3);
saveManager_.writeByte(poke.pp_move4);
saveManager_.writeByte(remainingEggCycles);
saveManager_.writeByte(poke.pokerus);
saveManager_.writeUint16(poke.caught_data, Endianness::BIG);
saveManager_.writeByte(poke.level);
if(entrySize_ == 48)
{
// party pokemon -> write "temporary" field values
gen2_recalculatePokeStats(gameReader_, poke);
poke.current_hp = poke.max_hp;
saveManager_.writeByte(poke.status_condition);
saveManager_.writeByte(poke.unused_byte);
saveManager_.writeUint16(poke.current_hp, Endianness::BIG);
saveManager_.writeUint16(poke.max_hp, Endianness::BIG);
saveManager_.writeUint16(poke.atk, Endianness::BIG);
saveManager_.writeUint16(poke.def, Endianness::BIG);
saveManager_.writeUint16(poke.speed, Endianness::BIG);
saveManager_.writeUint16(poke.special_atk, Endianness::BIG);
saveManager_.writeUint16(poke.special_def, Endianness::BIG);
}
return true;
}
bool Gen2PokemonList::add(const Gen2TrainerPokemon& poke, bool isEgg, const char* originalTrainerID, const char* nickname)
{
const uint8_t oldNumberOfPokemon = getNumberOfPokemon();
const uint8_t speciesListTerminator = 0xFF;
if(oldNumberOfPokemon + 1 > getMaxNumberOfPokemon())
{
return false;
}
// first add a terminator on the next entry
if(!setSpeciesAtIndex(oldNumberOfPokemon + 1, speciesListTerminator))
{
// failure
return false;
}
//now add the species index of the new pokemon at the end of the list
if(!setSpeciesAtIndex(oldNumberOfPokemon, poke.poke_index))
{
// failure
return false;
}
if(!setPokemon(oldNumberOfPokemon, poke, isEgg))
{
//failure: undo species index update by replacing it with the terminator
setSpeciesAtIndex(oldNumberOfPokemon, speciesListTerminator);
return false;
}
setOriginalTrainerOfPokemon(oldNumberOfPokemon, originalTrainerID);
if(isEgg)
{
setPokemonNickname(oldNumberOfPokemon, "EGG");
}
else
{
setPokemonNickname(oldNumberOfPokemon, nickname);
}
// if everything was successful, update the counter of the list
return setNumberOfPokemon(oldNumberOfPokemon + 1);
}
const char* Gen2PokemonList::getPokemonNickname(uint8_t index)
{
static char result[20];
const uint8_t pokeTextTerminator = 0x50;
uint8_t encodedNickName[NICKNAME_SIZE];
const uint16_t nicknameOffset = listCapacity_ + 2 + (listCapacity_ * entrySize_) + (listCapacity_ * ORIGINAL_TRAINER_NAME_SIZE) + (index * NICKNAME_SIZE);
if(!saveManager_.seek(getSaveOffset() + nicknameOffset))
{
return nullptr;
}
saveManager_.readUntil(encodedNickName, pokeTextTerminator, NICKNAME_SIZE);
gen2_decodePokeText(encodedNickName, sizeof(encodedNickName), result, sizeof(result));
return result;
}
bool Gen2PokemonList::setPokemonNickname(uint8_t index, const char* nickname)
{
uint8_t encodedNickName[NICKNAME_SIZE];
const uint8_t pokeTextTerminator = 0x50;
if(!nickname)
{
Gen2TrainerPokemon poke;
getPokemon(index, poke);
nickname = gameReader_.getPokemonName(poke.poke_index);
}
const uint16_t encodedLength = gen2_encodePokeText(nickname, strlen(nickname), encodedNickName, NICKNAME_SIZE, pokeTextTerminator);
const uint16_t nicknameOffset = listCapacity_ + 2 + (listCapacity_ * entrySize_) + (listCapacity_ * ORIGINAL_TRAINER_NAME_SIZE) + (index * NICKNAME_SIZE);
if(!saveManager_.seek(getSaveOffset() + nicknameOffset))
{
return false;
}
saveManager_.write(encodedNickName, encodedLength);
return true;
}
const char* Gen2PokemonList::getOriginalTrainerOfPokemon(uint8_t index)
{
static char result[20];
const uint8_t pokeTextTerminator = 0x50;
uint8_t encodedOTName[ORIGINAL_TRAINER_NAME_SIZE];
const uint16_t originalTrainerOffset = listCapacity_ + 2 + (listCapacity_ * entrySize_) + (index * ORIGINAL_TRAINER_NAME_SIZE);
if(!saveManager_.seek(getSaveOffset() + originalTrainerOffset))
{
return nullptr;
}
saveManager_.readUntil(encodedOTName, pokeTextTerminator, NICKNAME_SIZE);
gen2_decodePokeText(encodedOTName, sizeof(encodedOTName), result, sizeof(result));
return result;
}
bool Gen2PokemonList::setOriginalTrainerOfPokemon(uint8_t index, const char* originalTrainerID)
{
uint8_t encodedOTName[ORIGINAL_TRAINER_NAME_SIZE];
const uint8_t pokeTextTerminator = 0x50;
const uint16_t encodedLength = gen2_encodePokeText(originalTrainerID, strlen(originalTrainerID), encodedOTName, ORIGINAL_TRAINER_NAME_SIZE, pokeTextTerminator);
const uint16_t originalTrainerOffset = listCapacity_ + 2 + (listCapacity_ * entrySize_) + (index * ORIGINAL_TRAINER_NAME_SIZE);
if(!saveManager_.seek(getSaveOffset() + originalTrainerOffset))
{
return false;
}
saveManager_.write(encodedOTName, encodedLength);
return true;
}
Gen2Party::Gen2Party(Gen2GameReader& gameReader, ISaveManager& saveManager)
: Gen2PokemonList(gameReader, saveManager, PARTY_LIST_CAPACITY, PARTY_LIST_PKMN_ENTRY_SIZE)
{
}
Gen2Party::~Gen2Party()
{
}
uint32_t Gen2Party::getSaveOffset()
{
return (gameReader_.isGameCrystal()) ? 0x2865 : 0x288A;
}
Gen2Box::Gen2Box(Gen2GameReader& gameReader, ISaveManager& saveManager, uint8_t boxIndex)
: Gen2PokemonList(gameReader, saveManager, PC_BOX_LIST_CAPACITY, PC_BOX_LIST_PKMN_ENTRY_SIZE)
, boxIndex_(boxIndex)
{
}
Gen2Box::~Gen2Box()
{
}
uint32_t Gen2Box::getSaveOffset()
{
uint32_t result;
switch(boxIndex_)
{
case 0:
result = 0x4000;
break;
case 1:
result = 0x4450;
break;
case 2:
result = 0x48A0;
break;
case 3:
result = 0x4CF0;
break;
case 4:
result = 0x5140;
break;
case 5:
result = 0x5590;
break;
case 6:
result = 0x59E0;
break;
case 7:
result = 0x6000;
break;
case 8:
result = 0x6450;
break;
case 9:
result = 0x68A0;
break;
case 10:
result = 0x6CF0;
break;
case 11:
result = 0x7140;
break;
case 12:
result = 0x7590;
break;
case 13:
result = 0x79E0;
break;
default:
result = 0;
break;
}
return result;
}

View File

@ -0,0 +1,208 @@
#include "gen2/Gen2SpriteDecoder.h"
#include "RomReader.h"
// Lookup table for reversing bits of a byte
static const uint8_t reverse_table[256] = {
0, 128, 64, 192, 32, 160, 96, 224, 16, 144, 80, 208, 48, 176, 112, 240,
8, 136, 72, 200, 40, 168, 104, 232, 24, 152, 88, 216, 56, 184, 120, 248,
4, 132, 68, 196, 36, 164, 100, 228, 20, 148, 84, 212, 52, 180, 116, 244,
12, 140, 76, 204, 44, 172, 108, 236, 28, 156, 92, 220, 60, 188, 124, 252,
2, 130, 66, 194, 34, 162, 98, 226, 18, 146, 82, 210, 50, 178, 114, 242,
10, 138, 74, 202, 42, 170, 106, 234, 26, 154, 90, 218, 58, 186, 122, 250,
6, 134, 70, 198, 38, 166, 102, 230, 22, 150, 86, 214, 54, 182, 118, 246,
14, 142, 78, 206, 46, 174, 110, 238, 30, 158, 94, 222, 62, 190, 126, 254,
1, 129, 65, 193, 33, 161, 97, 225, 17, 145, 81, 209, 49, 177, 113, 241,
9, 137, 73, 201, 41, 169, 105, 233, 25, 153, 89, 217, 57, 185, 121, 249,
5, 133, 69, 197, 37, 165, 101, 229, 21, 149, 85, 213, 53, 181, 117, 245,
13, 141, 77, 205, 45, 173, 109, 237, 29, 157, 93, 221, 61, 189, 125, 253,
3, 131, 67, 195, 35, 163, 99, 227, 19, 147, 83, 211, 51, 179, 115, 243,
11, 139, 75, 203, 43, 171, 107, 235, 27, 155, 91, 219, 59, 187, 123, 251,
7, 135, 71, 199, 39, 167, 103, 231, 23, 151, 87, 215, 55, 183, 119, 247,
15, 143, 79, 207, 47, 175, 111, 239, 31, 159, 95, 223, 63, 191, 127, 255
};
// Function to reverse bits in a uint8_t
static uint8_t reverse_bits(uint8_t byte)
{
return reverse_table[byte];
}
Gen2SpriteDecoder::Gen2SpriteDecoder(IRomReader& romReader)
: romReader_(romReader)
, buffer_()
, cur_(buffer_)
{
}
Gen2SpriteDecoder::~Gen2SpriteDecoder()
{
}
uint8_t* Gen2SpriteDecoder::decode()
{
uint8_t byte;
uint8_t command;
uint16_t count;
cur_ = buffer_;
romReader_.readByte(byte);
while(byte != 0xFF)
{
command = (byte >> 5);
count = (byte & 0x1f);
decodeCommand(command, count);
romReader_.readByte(byte);
}
return buffer_;
}
uint8_t* Gen2SpriteDecoder::decode(uint8_t bankIndex, uint16_t pointer)
{
if (!romReader_.seekToRomPointer(pointer, bankIndex))
{
return nullptr;
}
return decode();
}
static void swap(uint8_t& b1, uint8_t& b2)
{
const uint8_t temp = b1;
b1 = b2;
b2 = temp;
}
/**
* Based on docs/gen2.txt and https://www.nesdev.org/wiki/Tile_compression
*/
void Gen2SpriteDecoder::decodeCommand(uint8_t command, uint16_t count)
{
uint8_t* end;
uint8_t byte1;
uint8_t byte2;
switch(command)
{
case 0:
// copy the next c + 1 bytes to the buffer
romReader_.read(cur_, count + 1);
cur_ += (count + 1);
break;
case 1:
{
// Read another byte, and write it to the output c + 1 times.
romReader_.readByte(byte1);
end = cur_ + (count + 1);
while(cur_ < end)
{
(*cur_) = byte1;
++cur_;
}
break;
}
case 2:
{
// Read another byte b1 and byte b2, and write byte b1 to the output c + 1 times, swapping b1 and b2 after each write.
romReader_.readByte(byte1);
romReader_.readByte(byte2);
end = cur_ + (count + 1);
while(cur_ < end)
{
(*cur_) = byte1;
++cur_;
swap(byte1, byte2);
}
break;
}
case 3:
// Write a zero byte (0x00) to the output c + 1 times.
end = cur_ + (count + 1);
while(cur_ < end)
{
(*cur_) = 0;
++cur_;
}
break;
case 4:
case 5:
case 6:
// process commands that reuse already decoded data
processReuseCommand(command, count);
break;
case 7:
romReader_.readByte(byte1);
command = (count >> 2);
count = ((count & 3) << 8) | byte1;
decodeCommand(command, count);
break;
}
}
/**
* Based on docs/gen2.txt and https://www.nesdev.org/wiki/Tile_compression
*/
void Gen2SpriteDecoder::processReuseCommand(uint8_t command, uint16_t count)
{
uint32_t offset;
uint8_t byte1;
uint8_t byte2;
uint8_t* readCur;
uint8_t* end;
// Read byte b. If b < 0x80, then read byte b2; offset is b×256+b2 bytes from the output stream beginning.
// Else offset = b bytes behind from the current output stream end.
romReader_.readByte(byte1);
if(byte1 < 0x80)
{
romReader_.readByte(byte2);
offset = (((uint16_t)byte1) * 256) + byte2;
readCur = buffer_ + offset;
}
else
{
offset = (byte1 & 0x7F);
readCur = cur_ - offset - 1;
}
switch(command)
{
case 4:
// Copy c + 1 bytes from the output stream at offset to the output.
// WARNING: this copy needs to be done byte per byte, as you can have an overlapping range with readCur and cur_
// (dugtrio, jigglypuff, ...). Using a single memcpy() instead causes graphical artefacts.
end = cur_ + (count + 1);
while(cur_ < end)
{
(*cur_) = (*readCur);
++readCur;
++cur_;
}
break;
case 5:
{
// Copy c + 1 bytes from the output stream at offset to the output, reversing the bits in each byte.
end = cur_ + (count + 1);
while(cur_ < end)
{
(*cur_) = reverse_bits((*readCur));
++readCur;
++cur_;
}
break;
}
case 6:
// Copy c + 1 bytes from the output stream at offset to the output, decrementing the read position after each write (i.e. reverse the data)
end = cur_ + (count + 1);
while(cur_ < end)
{
(*cur_) = (*readCur);
--readCur;
++cur_;
}
break;
}
}

300
src/utils.cpp Normal file
View File

@ -0,0 +1,300 @@
#include "utils.h"
#include "common.h"
#include <cstring>
#include <cstdio>
#include <cstdlib>
//#define PNG_SUPPORT
#ifdef PNG_SUPPORT
#include <png.h>
#endif
static const char* findCharsByTextCode(TextCodePair* textCodes, uint16_t numEntries, uint8_t code)
{
const TextCodePair* const end = textCodes + numEntries;
const TextCodePair* cur = textCodes;
while(cur < end)
{
if(cur->code == code)
{
return cur->chars;
}
++cur;
}
return nullptr;
}
static void findTextcodeByString(TextCodePair* textCodes, uint16_t numEntries, const char* input, uint16_t inputLength, uint8_t& outCode, uint16_t& needleLength)
{
const TextCodePair* const end = textCodes + numEntries;
const TextCodePair* cur = textCodes;
while(cur < end)
{
needleLength = strlen(cur->chars);
if(needleLength > inputLength)
{
++cur;
continue;
}
if(strncmp(input, cur->chars, needleLength) == 0)
{
outCode = cur->code;
return;
}
++cur;
}
needleLength = 0;
outCode = 0;
}
const uint8_t* memSearch(const uint8_t* haystack, uint32_t haystackLength, const uint8_t* needle, uint32_t needleLength)
{
const uint8_t* cur = haystack;
const uint8_t* const end = haystack + haystackLength - needleLength;
const uint8_t* ret;
while(cur < end)
{
ret = (const uint8_t*)memchr(haystack, needle[0], (end - cur));
if(!ret)
{
// the first byte of the needle is nowhere to be found.
return NULL;
}
if(!memcmp(ret, needle, needleLength))
{
// found the needle -> we can return this pointer as a result
return ret;
}
cur = ret + 1;
}
return NULL;
}
uint16_t decodeText(struct TextCodePair* textCodes, uint16_t numTextCodes, const uint8_t* inputBuffer, uint16_t inputBufferLength, char* outputBuffer, uint16_t outputBufferLength)
{
uint16_t result = 0;
if(inputBufferLength > outputBufferLength)
{
return result;
}
const uint8_t* curInput = (const uint8_t*)inputBuffer;
const uint8_t* endInput = curInput + inputBufferLength;
uint8_t* curOutput = (uint8_t*)outputBuffer;
uint16_t remainingBufferSpace = outputBufferLength;
while(curInput < endInput)
{
if((*curInput) == 0x50)
{
break;
}
else
{
const char* outputText = findCharsByTextCode(textCodes, numTextCodes, (*curInput));
const size_t outputTextLength = strlen(outputText);
if(remainingBufferSpace < outputTextLength + 1)
{
// not enough space left: truncated
memcpy(curOutput, outputText, remainingBufferSpace - 1);
outputBuffer[outputBufferLength -1] = '\0';
return outputBufferLength - 1;
}
memcpy(curOutput, outputText, outputTextLength);
curOutput += outputTextLength;
result += outputTextLength;
remainingBufferSpace -= outputTextLength;
++curInput;
}
}
// terminator byte
(*curOutput) = '\0';
return result;
}
uint16_t encodeText(struct TextCodePair* textCodes, uint16_t numTextCodes, const char* inputBuffer, uint16_t inputBufferLength, uint8_t* outputBuffer, uint16_t outputBufferLength, uint8_t terminator)
{
uint16_t needleLength;
uint8_t code;
const char* cur = inputBuffer;
const char* const inputBufferEnd = inputBuffer + inputBufferLength;
uint8_t* outputCur = outputBuffer;
uint8_t* const outputBufferEnd = outputBuffer + outputBufferLength;
bool match;
while(cur < inputBufferEnd)
{
match = false;
findTextcodeByString(textCodes, numTextCodes, cur, inputBufferLength - (cur - inputBuffer), code, needleLength);
if(code)
{
// match: every match results in 1 byte being added to the outputbuffer. That's the whole point of encoding this stuff
(*outputCur) = code;
if(outputCur == outputBufferEnd - 1)
{
// reached the end of the outputbuffer. So truncate
(*outputCur) = terminator;
// for consistency, we need to include the terminator in the byte count returned
++outputCur;
return static_cast<uint16_t>(outputCur - outputBuffer);
}
else
{
match = true;
++outputCur;
cur += needleLength;
}
}
if(!match)
{
printf("Error trying to encode text!\n");
break;
}
}
(*outputCur) = terminator;
// for consistency, we need to include the terminator in the byte count returned
++outputCur;
return static_cast<uint16_t>(outputCur - outputBuffer);
}
// the attribute stuff is to make sure the compiler doesn't optimize this code away.
__attribute__((optimize("O0")))
bool isCurrentCPULittleEndian()
{
short int word = 0x0001;
char *byte = (char *) &word;
return (byte[0] ? true : false);
}
uint16_t byteSwapUint16(uint16_t val)
{
return __builtin_bswap16(val);
}
uint32_t byteSwapUint32(uint32_t val)
{
return __builtin_bswap32(val);
}
bool write_png(const char *filename, unsigned char *rgb_data, int width, int height)
{
#ifdef PNG_SUPPORT
FILE *fp = fopen(filename, "wb");
if (!fp) {
perror("fopen");
return false;
}
png_structp png = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
if (!png) {
fclose(fp);
return false;
}
png_infop info = png_create_info_struct(png);
if (!info) {
png_destroy_write_struct(&png, NULL);
fclose(fp);
return false;
}
if (setjmp(png_jmpbuf(png))) {
png_destroy_write_struct(&png, &info);
fclose(fp);
return false;
}
png_init_io(png, fp);
// Set the image information
png_set_IHDR(png, info, width, height, 8, PNG_COLOR_TYPE_RGB,
PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
png_write_info(png, info);
// Write the image data
for (int y = 0; y < height; y++) {
png_bytep row_pointer = (png_bytep)(rgb_data + y * width * 3);
png_write_row(png, row_pointer);
}
png_write_end(png, NULL);
png_destroy_write_struct(&png, &info);
fclose(fp);
return true;
#else
(void)rgb_data;
(void)width;
(void)height;
fprintf(stderr, "Error: Can't write %s! PNG support was not enabled at build time!\n", filename);
return false;
#endif
}
uint8_t* readFileIntoBuffer(const char* pathToFile, uint32_t& outFileSize)
{
FILE* file = fopen(pathToFile, "rb");
if (!file) {
perror("Failed to open file");
return NULL;
}
// Seek to the end of the file to get the file size
if (fseek(file, 0, SEEK_END) != 0) {
perror("Failed to seek to end of file");
fclose(file);
return NULL;
}
const long size = ftell(file);
if (size == -1) {
perror("Failed to get file size");
fclose(file);
return NULL;
}
// Return to the beginning of the file
if (fseek(file, 0, SEEK_SET) != 0) {
perror("Failed to seek to beginning of file");
fclose(file);
return NULL;
}
// Allocate buffer to hold the file contents
uint8_t* buffer = (uint8_t*)malloc(size);
if (!buffer) {
perror("Failed to allocate buffer");
fclose(file);
return NULL;
}
// Read the file into the buffer
size_t bytesRead = fread(buffer, 1, size, file);
if (bytesRead != static_cast<uint32_t>(size)) {
perror("Failed to read file");
free(buffer);
fclose(file);
return NULL;
}
// Close the file and return the buffer
fclose(file);
outFileSize = static_cast<uint32_t>(size);
return buffer;
}