diff --git a/CREDITS.md b/CREDITS.md index bf0ae88..0bd81b8 100755 --- a/CREDITS.md +++ b/CREDITS.md @@ -1,5 +1,7 @@ # Credits +## Documentation / Other Projects + First of all, I want to thank the people behind [Bulbapedia](https://bulbapedia.bulbagarden.net/wiki/Main_Page). 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](https://www.youtube.com/c/retrogamemechanicsexplained) Youtube channel. His videos on the topic of Gen 1 sprite decompression were pretty helpful. @@ -18,3 +20,12 @@ And of course I'd like to extend my thanks to everyone else in the pokémunity w I've listed the specific resources I used for reading the rom/save game data in the CREDITS.md file of the libpokemegb project. +## Testing/Validation + +Thank you to all folks who reached out with feedback and who did some testing. Some even went above and beyond, therefore I would like to specifically name them here. + +/u/ImranFZakhaev + +/u/bluemooncinco + +MajorUpgrade diff --git a/Makefile b/Makefile index 1c97fc2..ef9b212 100755 --- a/Makefile +++ b/Makefile @@ -36,8 +36,12 @@ filesystem/%.sprite: assets/%.png @echo " [SPRITE] $@" $(N64_MKSPRITE) $(MKSPRITE_FLAGS) -o filesystem "$<" -filesystem/Arial.font64: MKFONT_FLAGS+=--size 11 --outline 1.0 --char-spacing 1.0 --range 20-E9 -filesystem/Arial-small.font64: MKFONT_FLAGS+=--size 10 --outline 1.0 --char-spacing 1.0 --range 20-E9 +# We also need Japanese characters +# I wrote a program to determine the character ranges for the japanese characters actually used by +# the gameboy games. +filesystem/Arial.font64: MKFONT_FLAGS+= -v --size 11 --outline 1.0 --char-spacing 1.0 --range 20-E9 --range 2026-2026 --range 25B6-25B7 --range 25BC-25BC --range 2640-2640 --range 2642-2642 --range 3002-3002 --range 300C-300F --range 3041-3089 --range 308B-308D --range 308F-308F --range 3092-309C --range 30A1-30D7 --range 30DB-30EF --range 30F2-30F4 --range 30FB-30FC --range 5186-5186 --range FF1A-FF1A + +filesystem/Arial-small.font64: MKFONT_FLAGS+=-v --size 10 --outline 1.0 --char-spacing 1.0 --range 20-E9 --range 2026-2026 --range 25B6-25B7 --range 25BC-25BC --range 2640-2640 --range 2642-2642 --range 3002-3002 --range 300C-300F --range 3041-3089 --range 308B-308D --range 308F-308F --range 3092-309C --range 30A1-30D7 --range 30DB-30EF --range 30F2-30F4 --range 30FB-30FC --range 5186-5186 --range FF1A-FF1A filesystem/logo-libdragon.sprite: MKSPRITE_FLAGS += -f RGBA32 filesystem/logo-bulbagarden.sprite: MKSPRITE_FLAGS += -f RGBA32 @@ -62,4 +66,4 @@ clean: rm -rf filesystem/* .PHONY: clean --include $(wildcard $(BUILD_DIR)/*.d) \ No newline at end of file +-include $(wildcard $(BUILD_DIR)/*.d) diff --git a/README.md b/README.md index dc5fcd4..6132012 100755 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ This project lets you acquire past Distribution Event Pokémon in Gen1/Gen2 Pok The rom is based on [libpokemegb](https://github.com/risingPhil/libpokemegb) and [libdragon](https://github.com/DragonMinded/libdragon). -![Screenshot 1](docs/images/screen1.png)![Screenshot 2](docs/images/screen2.png)![Screenshot 3](docs/images/screen3.png)![Screenshot 4](docs/images/screen4.png)![Screenshot 5](docs/images/screen5.png)![Screenshot 6](docs/images/screen6.png)![Screenshot 7](docs/images/screen7.png)![Screenshot 8](docs/images/screen8.png) +![Screenshot 1](docs/images/screen1.png)![Screenshot 2](docs/images/screen2.png)![Screenshot 9](docs/images/screen9.png)![Screenshot 3](docs/images/screen3.png)![Screenshot 4](docs/images/screen4.png)![Screenshot 5](docs/images/screen5.png)![Screenshot 6](docs/images/screen6.png)![Screenshot 8](docs/images/screen8.png)![Screenshot 7](docs/images/screen7.png) Right now the UI is still barebones. I aim to improve this in future versions. @@ -12,9 +12,10 @@ I'm happy to accept pull requests if the community wants to do them. # Current Features - Inject Generation 1 Distribution event Pokémon into Gen 1 cartridges. In practice, this just means all kinds of variants of Mew. -- Inject Generation 2 Distribution event Pokémon into Gen 2 cartridges. This includes the Pokémon Center New York (PCNY) Distribution event ones. +- Inject Generation 2 Distribution event Pokémon into Gen 2 cartridges. This includes the Japanese and Pokémon Center New York (PCNY) Distribution event ones! - Inject GS Ball into an actual Pokémon Crystal gameboy cartridge - Teach Pikachu Surf/Fly on Gen 1 cartridges +- Gen I move deleter - You don't have to use the transfer pak in controller 1. You can have it in a separate controller if you want. But the UI is still controlled with controller 1. - Unlock Mystery Gift decorations like the Pikachu Bed and Tentacool Doll that were left inaccessible in Gold/Silver/Crystal due to bugs in Pokemon Stadium 2 (suggested by /u/MermaidRaccoon on reddit) - Make it possible to backup your cartridge save file onto the flashcart PokeMe64 is running from. @@ -23,11 +24,15 @@ I'm happy to accept pull requests if the community wants to do them. - Make it easy to reset/reconfigure the Generation II game clock # Limitations -- Right now, this rom only supports the international (English) versions of the games. +- When using a Korean gen II pokémon game, neither the trainer name (in various screens) nor the pokémon name (in the stats screen) will be displayed in the UI. This is because our font doesn't have the Korean characters and I don't sufficiently care to add them because they're such niche versions. -But I've heard from /u/bluemooncinco on reddit that PokeMe64 crashes on gen 1 French and Spanish, but Spanish gen 2 games appear to work. But your mileage may vary, as I haven't personally tested any games besides the international (English) ones. The main reason why other languages may or may not work is because the relevant data is at different positions inside the game data. +- When adding a Japanese pokémon to a non-Japanese cart, the trainer name will be replaced with "PM64". This is because the non-japanese games don't include the Japanese character sets, so they can't display them. -PokeMe64 also doesn't support batteryless reproduction carts. (see "Reproduction carts" below) +- When adding a non-Japanese pokémon to a Japanese cart, the trainer name will be replaced with "ポケメ64". (which, according to Google Translate, translates to Pokeme 64) + +- PokeMe64 also doesn't support batteryless reproduction carts due to technical limitations. Unfortunately this means most reproduction carts can't be supported. (see the "Reproduction carts" section below). + +- Flashcarts also unfortunately can't be supported, because those load into a menu first. (and likely also run into the same hardware limitation as with batteryless repro carts) # Build @@ -68,9 +73,9 @@ But having it done with a Nintendo 64 feels more "real"/"official" and is easier ## Feature ideas - [~~Support batteryless reproduction cartridges~~](docs/Why_I_Had_To_Give_Up_On_Batteryless_Repros.md) -- Support other language versions (in libpokemegb) - Make it possible to display your cartridge save file as a QR code and contribute to the 3DS' [PKSM](https://github.com/FlagBrew/PKSM) project to migrate the save file easily from gameboy cartridge to 3DS. - Make it possible to swap gameboy cartridge after using the reset button on the N64. (suggested by /u/bluemooncinco on reddit) +- Implement mystery gift. (idea by MajorUpgrade) I'm likely going to postpone the 3D stuff (intro and "trade" sequence) until I have implemented a lot of the other ideas here. diff --git a/assets/Arial-small.ttf b/assets/Arial-small.ttf deleted file mode 100755 index 7ff88f2..0000000 Binary files a/assets/Arial-small.ttf and /dev/null differ diff --git a/assets/Arial-small.ttf b/assets/Arial-small.ttf new file mode 120000 index 0000000..e626d28 --- /dev/null +++ b/assets/Arial-small.ttf @@ -0,0 +1 @@ +Arial.ttf \ No newline at end of file diff --git a/assets/Arial.ttf b/assets/Arial.ttf old mode 100755 new mode 100644 index 7ff88f2..ab17e02 Binary files a/assets/Arial.ttf and b/assets/Arial.ttf differ diff --git a/assets/cartridge-label-green.png b/assets/cartridge-label-green.png new file mode 100644 index 0000000..02e7afc Binary files /dev/null and b/assets/cartridge-label-green.png differ diff --git a/docs/Why_I_Had_To_Give_Up_On_Batteryless_Repros.md b/docs/Why_I_Had_To_Give_Up_On_Batteryless_Repros.md index 306740b..071515e 100644 --- a/docs/Why_I_Had_To_Give_Up_On_Batteryless_Repros.md +++ b/docs/Why_I_Had_To_Give_Up_On_Batteryless_Repros.md @@ -8,13 +8,13 @@ Alas, I've hit a bump in the road I fear I'm unable to overcome. (more on that l This is a bit of a bummer to me. Because instead of making repro carts slightly more "feature complete" by allowing you to get these event pokémon with PokeMe64 on those as well, I have ended up validating the cost of buying an original cartridge instead. -Well, I suppose that is not entirely true. Reproduction carts which actually HAVE battery-backed SRAM and which therefore don't use any of these batteryless save patches probably already work out of the box with PokeMe64 already, because they'd be indistinguishable from the real thing from the Nintendo 64 Transfer Pak's perspective. Those likely also work with the stadium games. +Well, I suppose that is not entirely true. Reproduction carts which actually HAVE battery-backed SRAM and which therefore don't use any of these batteryless save patches probably already work out of the box with PokeMe64. They'd be indistinguishable from the real thing from the Nintendo 64 Transfer Pak's perspective. Those likely also work with the stadium games. In fact, I've read posts that people have managed to convert a batteryless reproduction/bootleg to a battery-backed one by soldering a battery and re-flashing the repro cart with a cartridge flasher such as GBxCart RW. If this can be done, those converted reproduction carts should theoretically work with PokeMe64 (AND the stadium games). ## How reproduction cartridges work -As you may or may not have read already, reproduction cartridges usually don't have a battery embedded on the cartridge board. Instead, they have specific patches applied to use unused sections of the rom data to store the save data instead. That means that these cartridges actually re-flash part of their rom to store the save data when you try to save in-game. And when the game is starting, it copies the save data from flash to SRAM. (since there's no battery, SRAM loses its content every time the gameboy powers off). +Reproduction cartridges usually don't have a battery embedded on the cartridge board. Instead, they have specific patches applied to use unused sections of the rom data to store the save data instead. That means that these cartridges re-flash part of their rom to store the save data when you try to save in-game. And when the game is starting, it copies the save data from flash to SRAM. (since there's no battery, SRAM loses its content every time the gameboy powers off). The patches applied to these repros are so-called "batteryless save patches". In fact, there's a whole community out there (check the [BennVenn discord](https://discord.gg/KYxpSRgB)) that is actually backing up such patches and writing new ones, because they like having cartridges that don't require battery replacements. @@ -24,14 +24,12 @@ Now, while you might think writing to a flashable cartridge would be a simple ma Because the memory address ranges for reading gameboy rom data are not supposed to be writable, they are actually reused for different things when writing to the same addresses. -For instance, the memory range 0x0000-0x4000 would map to ROM bank 0 when you'd try to read it. However, on MBC3 cartridges, if you try to write to 0x0000-0x1FFF, you would change the RAM/RTC Enable register and writing to 0x2000-0x4000 would swap the current rom bank. +For instance, the memory range 0x0000-0x4000 would map to ROM bank 0 when you'd try to read it. However, on MBC3 cartridges, if you try to _write_ to 0x0000-0x1FFF, you would change the RAM/RTC Enable register and writing to 0x2000-0x4000 would swap the current rom bank. -Instead, reproduction/bootleg cartridge types have specific commands to trigger erase/flash operations on their flash chip. So far, I've seen that those commands are triggered by writing specific values to specific memory addresses. And these memory addresses are actually overlapping with existing MBC3 registers. +Instead, reproduction/bootleg cartridge types have specific commands to trigger erase/flash operations on their flash chip. So far, I've seen that those commands are triggered by writing specific values to specific memory addresses. And these memory addresses are actually overlapping with existing MBC3 registers used for other commands. People have managed to reflash entire bootleg/reproduction cartridges with completely different games (once again, check the [BennVenn discord](https://discord.gg/KYxpSRgB)) using some of the cartridge flashers (such as GbxCart RW). -I'm not entirely sure how these cartridge flashers work. (I didn't research that in depth.) However, while they probably use the aforementioned commands, I have also seen that there are cartridges out there that may use Pin 31 (AUDIO IN) to put the cartridge in flash mode. Then again, the latter does not seem to be relevant, because the gameboy can't change the signal of PIN 31, so it would still need separate commands to trigger the flash/erase operations while running the cartridge in the gameboy. - ## The problem(s) ### Non-showstoppers @@ -46,7 +44,7 @@ In fact, I found [this](https://github.com/tyllmoritz/game-boy-batteryless-patch And for figuring out the exact save data location, I could've made PokeMe64 ask for the trainer name and trainer id to search the rom for the right location. -So I already had some ideas on how to overcome the aforementioned challenges. +So I already had some ideas on how to overcome these challenges. ### The showstopper @@ -60,7 +58,7 @@ During my research I also stumbled onto [this blogpost](https://dhole.github.io/ But since the Nintendo 64 Transfer pak can only do 32-byte block writes, I can forget about being able to finish any of these writes in a single "bus cycle". (not to mention that the accidental write to neighbouring memory addresses may just break the sequence). -This is not just theoretical. I have tried writing code to simulate the batteryless patch that I dumped from my brothers' reproduction cart. But I also tried coding the wram\_bootleg\_read\_identifier method of [https://github.com/tyllmoritz/game-boy-batteryless-patcher/blob/0fbe8fecc8dc4b7ff0cdc9d7f7a3c19156798eab/src/batteryless.asm](https://github.com/tyllmoritz/game-boy-batteryless-patcher/blob/0fbe8fecc8dc4b7ff0cdc9d7f7a3c19156798eab/src/batteryless.asm). Neither of those attempts did what they were supposed to do. +This is not just theoretical. I have tried writing code to simulate the batteryless patch which I dumped earlier. But I also tried coding the wram\_bootleg\_read\_identifier method of [https://github.com/tyllmoritz/game-boy-batteryless-patcher/blob/0fbe8fecc8dc4b7ff0cdc9d7f7a3c19156798eab/src/batteryless.asm](https://github.com/tyllmoritz/game-boy-batteryless-patcher/blob/0fbe8fecc8dc4b7ff0cdc9d7f7a3c19156798eab/src/batteryless.asm). Neither of those attempts did what they were supposed to do. ## Conclusion diff --git a/docs/images/screen1.png b/docs/images/screen1.png index 3c63050..48ea340 100644 Binary files a/docs/images/screen1.png and b/docs/images/screen1.png differ diff --git a/docs/images/screen2.png b/docs/images/screen2.png index 31fa9b0..d2e727d 100644 Binary files a/docs/images/screen2.png and b/docs/images/screen2.png differ diff --git a/docs/images/screen7.png b/docs/images/screen7.png index 5fd7145..302885a 100644 Binary files a/docs/images/screen7.png and b/docs/images/screen7.png differ diff --git a/docs/images/screen9.png b/docs/images/screen9.png new file mode 100644 index 0000000..2685bc5 Binary files /dev/null and b/docs/images/screen9.png differ diff --git a/include/menu/MenuEntries.h b/include/menu/MenuEntries.h index 4fd8dd7..4038a2b 100755 --- a/include/menu/MenuEntries.h +++ b/include/menu/MenuEntries.h @@ -6,6 +6,9 @@ extern MenuItemData gen1MenuEntries[]; extern const uint32_t gen1MenuEntriesSize; +extern MenuItemData gen1MovesMenuEntries[]; +extern const uint32_t gen1MovesMenuEntriesSize; + extern MenuItemData gen2MenuEntries[]; extern const uint32_t gen2MenuEntriesSize; diff --git a/include/menu/MenuFunctions.h b/include/menu/MenuFunctions.h index e0c84c0..d68db3d 100755 --- a/include/menu/MenuFunctions.h +++ b/include/menu/MenuFunctions.h @@ -23,26 +23,96 @@ extern const uint16_t GEN2_EVENTFLAG_DECORATION_TENTACOOL_DOLL; void activateFrameLog(void* context, const void* param); void advanceDialog(void* context, const void* param); +// UI navigation functions void goToTestScene(void* context, const void* param); void goToPokeTransporterGBRef(void* context, const void* param); void goToAboutScene(void* context, const void* param); void goToDataCopyScene(void* context, const void* param); - +void goToGen1MovesMenu(void* context, const void* param); void goToGen1DistributionPokemonMenu(void* context, const void* param); void goToGen2DistributionPokemonMenu(void* context, const void* param); void goToGen2PCNYDistributionPokemonMenu(void* context, const void* param); void goToGen2DecorationMenu(void* context, const void* param); void goToBackupRestoreMenu(void* context, const void* param); +/** + * The first step to teach pikachu something. In this step we find a pikachu in the Trainer's + * party and check if there's currently room to add the new move. + * + * If not, we will prompt the user to select a move to replace. + * + * When the selection of the move index has been made, we'll call gen1TeachPikachu() + * + * @param context a MenuScene* + * @param param a const Move* indicating which move to teach + */ void gen1PrepareToTeachPikachu(void* context, const void* param); + +/** + * This function actually sets the given Move at the specified index in the party pokémon's data + * and saves the game. + * + * @param context a MenuScene* + * @param param a Gen1TeachPikachuParams struct pointer + */ void gen1TeachPikachu(void* context, const void* param); + +/** + * This function is the first stage of the move deleter functionality. + * It gives the user a prompt to select the pokémon in the trainer's party + * from which to delete a move. + * + * @param context a MenuScene* + * @param param a nullpointer (dummy param) + */ +void gen1MoveDeleterSelectPokemon(void* context, const void* param); + +/** + * This function is the second stage of the move deleter functionality. + * It gives the user a prompt to select the move of the selected pokémon to delete + * + * @param context a MenuScene* + * @param param a uint8_t value stored as the const void* value + */ +void gen1MoveDeleterSelectMove(void* context, const void* param); + +/** + * This function is the third stage of the move deleter functionality. + * It applies the move deletion to the selected pokémon. + * + * @param context a MenuScene* + * @param param a MoveDeleterParams struct pointer + */ +void gen1MoveDeleterApply(void* context, const void* param); + +/** + * This function will unlock the GS Ball event in a Pokémon Crystal save + * + * @param context a MenuScene* + * @param param a nullpointer (dummy param) + */ void gen2ReceiveGSBall(void* context, const void* param); +/** + * This function will set a gen II event flag + * + * @param context a MenuScene* + * @param param a uint16_t* which indicates the event flag index + */ void gen2SetEventFlag(void* context, const void* param); + +/** + * This function will ask for confirmation to wipe a save from the cartridge. + * + * If confirmed by the user, the DataCopyScene will be navigated to with the DATACOPY_WIPE_SAVE command + */ void askConfirmationWipeSave(void* context, const void* param); /** * This function will change an SRAM field to let gen 2 games prompt you * to reconfigure the game clock + * + * @param context a MenuScene* context + * @param param a nullpointer (dummy param) */ void resetRTC(void* context, const void* param); diff --git a/include/scenes/AboutScene.h b/include/scenes/AboutScene.h index 5357b99..5cf655f 100644 --- a/include/scenes/AboutScene.h +++ b/include/scenes/AboutScene.h @@ -22,8 +22,8 @@ public: void render(RDPQGraphics& gfx, const Rectangle& sceneBounds) override; protected: private: - uint8_t fontIdArial_; - uint8_t fontArialStyleWhiteId_; + uint8_t fontIdMainFont_; + uint8_t fontMainFontStyleWhiteId_; sprite_t* logoLibDragon_; sprite_t* logoBulbagarden_; sprite_t* logoRetroGameMechanicsExplained_; diff --git a/include/scenes/IScene.h b/include/scenes/IScene.h index 78862b2..627acfd 100755 --- a/include/scenes/IScene.h +++ b/include/scenes/IScene.h @@ -35,6 +35,7 @@ typedef struct SceneDependencies char playerName[16]; uint8_t generation; uint8_t specificGenVersion; + uint8_t localization; } SceneDependencies; class IScene diff --git a/include/scenes/InitTransferPakScene.h b/include/scenes/InitTransferPakScene.h index 0a6ccf1..37118e8 100755 --- a/include/scenes/InitTransferPakScene.h +++ b/include/scenes/InitTransferPakScene.h @@ -38,6 +38,8 @@ private: DialogData diagData_; TextRenderSettings pokeMe64TextSettings_; const char* gameTypeString_; + // build up a random seed for srand(). I'm doing this in an attempt to truly randomize IVs and shininess. + unsigned int randomSeed_; }; #endif \ No newline at end of file diff --git a/include/scenes/PokeTransporterGBRefScene.h b/include/scenes/PokeTransporterGBRefScene.h index e17e9af..ff0d362 100644 --- a/include/scenes/PokeTransporterGBRefScene.h +++ b/include/scenes/PokeTransporterGBRefScene.h @@ -22,8 +22,8 @@ private: sprite_t* pokeTransporterGBLogoSprite_; sprite_t* qrCodeSprite_; sprite_t* fennelPictureSprite_; - uint8_t fontArialSmallId_; - uint8_t fontArialSmallWhiteId_; + uint8_t fontMainFontSmallId_; + uint8_t fontMainFontSmallWhiteId_; }; #endif \ No newline at end of file diff --git a/include/scenes/SceneWithDialogWidget.h b/include/scenes/SceneWithDialogWidget.h index b7116ba..b82f7b4 100755 --- a/include/scenes/SceneWithDialogWidget.h +++ b/include/scenes/SceneWithDialogWidget.h @@ -23,7 +23,7 @@ protected: DialogWidget dialogWidget_; WidgetFocusChainSegment dialogFocusChainSegment_; - uint8_t arialId_; + uint8_t mainFontId_; uint8_t fontStyleWhiteId_; uint8_t fontStyleYellowId_; private: diff --git a/include/scenes/StatsScene.h b/include/scenes/StatsScene.h index 6fa45c1..421ca92 100644 --- a/include/scenes/StatsScene.h +++ b/include/scenes/StatsScene.h @@ -48,8 +48,8 @@ private: Gen2GameReader gen2GameReader_; sprite_t* menu9SliceSprite_; SpriteRenderSettings backgroundRenderSettings_; - uint8_t fontArialSmallId_; - uint8_t fontArialSmallWhiteId_; + uint8_t fontMainFontSmallId_; + uint8_t fontMainFontSmallWhiteId_; TextRenderSettings textSettings_; TextRenderSettings smallTextSettings_; TextRenderSettings statsSettings_; diff --git a/include/transferpak/TransferPakDataCopier.h b/include/transferpak/TransferPakDataCopier.h index 503a3ec..aed4db1 100644 --- a/include/transferpak/TransferPakDataCopier.h +++ b/include/transferpak/TransferPakDataCopier.h @@ -151,7 +151,7 @@ private: class TransferPakFileCopyDestination : public ITransferPakDataCopyDestination { public: - TransferPakFileCopyDestination(const char *pathOnSDCard); + TransferPakFileCopyDestination(const char *pathOnSDCard, bool resetRTC = false); virtual ~TransferPakFileCopyDestination(); bool readyForTransfer() const override; @@ -167,6 +167,7 @@ protected: private: FILE *outputFile_; uint32_t bytesWritten_; + bool resetRTC_; }; /** diff --git a/include/transferpak/TransferPakManager.h b/include/transferpak/TransferPakManager.h index 830f656..cc2a706 100755 --- a/include/transferpak/TransferPakManager.h +++ b/include/transferpak/TransferPakManager.h @@ -66,6 +66,22 @@ public: */ void switchGBSRAMBank(uint8_t bankIndex); + /** + * For MBC1 controllers, this will effect the ROM or RAM modes, depending on what is written in $6000-$7FFF. + * If the mode is 0 (default), the RAM bank will be locked to 0, but the extended banks (bankindex > 0x1F) + * can be accessed in the 0x0000 - 0x3FFF range (thereby replacing bank 0) by modifying the 0x4000-0x5FFF registers. + * However, in this mode, the RAM bank is locked to bank 0 + * + * If the mode is 1, the RAM bank can be switched, but the extended rom banks (bankindex > 0x1F) can't be switched to. + * In this mode 0x0000 - 0x3FFF is locked to rom bank 0. + * Sources: + * - https://retrocomputing.stackexchange.com/questions/11732/how-does-the-gameboys-memory-bank-switching-work + * - https://gbdev.io/pandocs/MBC1.html + * + * For our use case though, we don't need mode 0. + */ + void switchMBC1BankingMode(uint8_t mode); + /** * @brief This function reads data from the specified gameboy address */ diff --git a/include/widget/PokemonPartyIconWidget.h b/include/widget/PokemonPartyIconWidget.h index aaedd80..6c588ca 100644 --- a/include/widget/PokemonPartyIconWidget.h +++ b/include/widget/PokemonPartyIconWidget.h @@ -26,7 +26,7 @@ public: PokemonPartyIconFactory(IRomReader& romReader); ~PokemonPartyIconFactory(); - surface_t getIcon(uint8_t generation, uint8_t specificGenType, uint8_t iconType, bool firstFrame); + surface_t getIcon(uint8_t generation, uint8_t specificGenType, uint8_t localization, uint8_t iconType, bool firstFrame); protected: private: // This is the map of icons. Gen2 has the most icons, that's why I'm using GEN2_ICONTYPE_MAX as the array size @@ -66,6 +66,11 @@ typedef struct PokemonPartyIconWidgetData * The specific variant of the game */ uint8_t specificGenVersion; + + /** + * @brief The specific localization of the game + */ + uint8_t localization; /** * The pokemon icon type we need to display */ diff --git a/libpokemegb b/libpokemegb index 2ba33f2..aedb588 160000 --- a/libpokemegb +++ b/libpokemegb @@ -1 +1 @@ -Subproject commit 2ba33f26f772b261050cd98e035fcab47e0aaba9 +Subproject commit aedb58832c188d3c7ce28da6bc1c453c128ec1be diff --git a/src/menu/MenuEntries.cpp b/src/menu/MenuEntries.cpp index 778b406..6e79f43 100755 --- a/src/menu/MenuEntries.cpp +++ b/src/menu/MenuEntries.cpp @@ -11,14 +11,8 @@ MenuItemData gen1MenuEntries[] = { .onConfirmAction = goToGen1DistributionPokemonMenu }, { - .title = "Teach Pikachu Surf", - .onConfirmAction = gen1PrepareToTeachPikachu, - .itemParam = &MOVE_SURF - }, - { - .title = "Teach Pikachu Fly", - .onConfirmAction = gen1PrepareToTeachPikachu, - .itemParam = &MOVE_FLY + .title = "Pokémon Moves", + .onConfirmAction = goToGen1MovesMenu }, { .title = "Gen 3 Transfer Info", @@ -32,6 +26,25 @@ MenuItemData gen1MenuEntries[] = { const uint32_t gen1MenuEntriesSize = sizeof(gen1MenuEntries); +MenuItemData gen1MovesMenuEntries[] = { + { + .title = "Teach Pikachu Surf", + .onConfirmAction = gen1PrepareToTeachPikachu, + .itemParam = &MOVE_SURF + }, + { + .title = "Teach Pikachu Fly", + .onConfirmAction = gen1PrepareToTeachPikachu, + .itemParam = &MOVE_FLY + }, + { + .title = "Delete Move", + .onConfirmAction = gen1MoveDeleterSelectPokemon + } +}; + +const uint32_t gen1MovesMenuEntriesSize = sizeof(gen1MovesMenuEntries); + MenuItemData gen2MenuEntries[] = { { .title = "Backup/Restore", diff --git a/src/menu/MenuFunctions.cpp b/src/menu/MenuFunctions.cpp index 7fe31b9..0b23125 100755 --- a/src/menu/MenuFunctions.cpp +++ b/src/menu/MenuFunctions.cpp @@ -28,12 +28,36 @@ const uint16_t GEN2_EVENTFLAG_DECORATION_TENTACOOL_DOLL = 715; typedef struct Gen1TeachPikachuParams { + /** + * The move to teach + */ Move moveType; + /** + * The index of the pokémon in the party + */ uint8_t partyIndex; + /** + * The index of the move slot in which the move should be inserted + */ uint8_t moveIndex; + /** + * The data structure of the pokemon we're trying to teach the move + */ Gen1TrainerPokemon poke; } Gen1TeachPikachuParams; +/** + * This structure is used throughout the move deleter process + * to pass the required parameters + */ +typedef struct MoveDeleterParams +{ + // the index of the selected pokemon in the party + uint8_t partyIndex; + // the index of the selected move + uint8_t moveIndex; +} MoveDeleterParams; + // here are a few structs I use to pass around parameters to the gen1TeachPikachu() function // i want to avoid dynamic allocation for this purpose. // we need 4 of them because we will have 4 menu options, each with a different moveIndex @@ -168,6 +192,17 @@ void goToDataCopyScene(void* context, const void* param) } } +void goToGen1MovesMenu(void* context, const void* param) +{ + MenuScene* scene = static_cast(context); + auto newSceneContext = new MenuSceneContext{ + .menuEntries = gen1MovesMenuEntries, + .numMenuEntries = gen1MovesMenuEntriesSize / sizeof(gen1MovesMenuEntries[0]) + }; + + scene->getDependencies().sceneManager.switchScene(SceneType::MENU, deleteMenuSceneContext, newSceneContext); +} + void goToGen1DistributionPokemonMenu(void* context, const void*) { goToDistributionPokemonListMenu(context, DistributionPokemonListType::GEN1); @@ -211,7 +246,9 @@ void gen1PrepareToTeachPikachu(void* context, const void* param) TransferPakManager& tpakManager = scene->getDependencies().tpakManager; TransferPakRomReader romReader(tpakManager); TransferPakSaveManager saveManager(tpakManager); - Gen1GameReader gameReader(romReader, saveManager, static_cast(scene->getDependencies().specificGenVersion)); + const Gen1GameType gameType = static_cast(scene->getDependencies().specificGenVersion); + const Gen1LocalizationLanguage language = static_cast(scene->getDependencies().localization); + Gen1GameReader gameReader(romReader, saveManager, gameType, language); DialogData* msg1 = nullptr; DialogData* msg2 = nullptr; uint8_t foundIndex; @@ -328,7 +365,9 @@ void gen1TeachPikachu(void* context, const void* param) TransferPakManager& tpakManager = scene->getDependencies().tpakManager; TransferPakRomReader romReader(tpakManager); TransferPakSaveManager saveManager(tpakManager); - Gen1GameReader gameReader(romReader, saveManager, static_cast(scene->getDependencies().specificGenVersion)); + const Gen1GameType gameType = static_cast(scene->getDependencies().specificGenVersion); + const Gen1LocalizationLanguage language = static_cast(scene->getDependencies().localization); + Gen1GameReader gameReader(romReader, saveManager, gameType, language); const Gen1TeachPikachuParams* params = static_cast(param); Gen1TrainerPokemon poke = params->poke; @@ -404,13 +443,258 @@ void gen1TeachPikachu(void* context, const void* param) scene->showDialog(msg1); } +void gen1MoveDeleterSelectPokemon(void* context, const void* param) +{ + DialogData* msg = nullptr; + MenuScene* scene = static_cast(context); + TransferPakManager& tpakManager = scene->getDependencies().tpakManager; + TransferPakRomReader romReader(tpakManager); + TransferPakSaveManager saveManager(tpakManager); + const Gen1GameType gameType = static_cast(scene->getDependencies().specificGenVersion); + const Gen1LocalizationLanguage language = static_cast(scene->getDependencies().localization); + Gen1GameReader gameReader(romReader, saveManager, gameType, language); + + // statically allocated storage for storing up to 6 pokemon nicknames. + // allocated statically to avoid having to care about memory management when passing them as a dialog option. + // see below for the reason why we need to do it like this. + static char nickNames[6][11]; + const char* curNickname; + + tpakManager.setRAMEnabled(true); + + Gen1Party party = gameReader.getParty(); + const uint8_t numberOfPokemon = party.getNumberOfPokemon(); + + msg = new DialogData { + .options = { + .items = new MenuItemData[numberOfPokemon + 1], + .number = (uint8_t)(numberOfPokemon + 1), + .shouldDeleteWhenDone = true + }, + .shouldDeleteWhenDone = true + }; + + if(!gen1_isAPokeCenter(gameReader.getCurrentMap())) + { + delete[] msg->options.items; + msg->options = {0}; + tpakManager.setRAMEnabled(false); + + setDialogDataText(*msg, "Sorry! You need to save in a Pokémon Center first!"); + scene->showDialog(msg); + return; + } + + for(uintptr_t i=0; i < numberOfPokemon; ++i) + { + // every call to getPokemonNickname overwrites the data of the last one + // because the same internal buffer is getting reused on every call. + // since we want to preserve that data, we need to copy it before consumption. + curNickname = party.getPokemonNickname(i); + strncpy(nickNames[i], curNickname, 11); + msg->options.items[i] = { + .title = nickNames[i], + .onConfirmAction = gen1MoveDeleterSelectMove, + .context = context, + .itemParam = (const void*)i + }; + } + + tpakManager.setRAMEnabled(false); + + // the last option in the option menu should be the Cancel option. + msg->options.items[numberOfPokemon] = { + .title = "Cancel", + .onConfirmAction = advanceDialog, + .context = context + }; + + setDialogDataText(*msg, "Which Pokémon should be made to forget a move?"); + scene->showDialog(msg); +} + +void gen1MoveDeleterSelectMove(void* context, const void* param) +{ + // We statically allocate a MoveDeleterParams struct for every possible move index. + // This is because we need to pass this struct as an itemParam for every option in the dialog options. + // by having it statically allocated, we don't have to worry about memory managing these instances. + // But it does mean they're allocated at all times during the runtime of PokeMe64. + // Therefore we're "wasting" 8 bytes while not being used. + static MoveDeleterParams moveIndexParams[4]; + + Gen1TrainerPokemon poke; + DialogData* msg = nullptr; + MenuScene* scene = static_cast(context); + const uint8_t partyIndex = (const uintptr_t)param; + uint8_t i; + uint8_t moveIndex; + + TransferPakManager& tpakManager = scene->getDependencies().tpakManager; + TransferPakRomReader romReader(tpakManager); + TransferPakSaveManager saveManager(tpakManager); + const Gen1GameType gameType = static_cast(scene->getDependencies().specificGenVersion); + const Gen1LocalizationLanguage language = static_cast(scene->getDependencies().localization); + Gen1GameReader gameReader(romReader, saveManager, gameType, language); + + tpakManager.setRAMEnabled(true); + + Gen1Party party = gameReader.getParty(); + + if(!party.getPokemon(partyIndex, poke, false)) + { + // Could not retrieve pokémon from party at the given index. + // this should never happen! + tpakManager.setRAMEnabled(false); + return; + } + + // fill up the moveIndexParams + for(i=0; i < 4; ++i) + { + moveIndexParams[i] = { + .partyIndex = partyIndex, + .moveIndex = i + }; + } + + msg = new DialogData { + .options = { + .items = new MenuItemData[5], + .number = 0, + .shouldDeleteWhenDone = true + }, + .shouldDeleteWhenDone = true + }; + + // avoid code duplication by adding the index fields into a uint8_t* array + const uint8_t* moveIndices[] = { &poke.index_move1, &poke.index_move2, &poke.index_move3, &poke.index_move4 }; + for(i = 0; i < 4; ++i) + { + moveIndex = *(moveIndices[i]); + if(moveIndex) + { + msg->options.items[msg->options.number] = { + .title = getMoveString((Move)moveIndex), + .onConfirmAction = gen1MoveDeleterApply, + .context = context, + .itemParam = (moveIndexParams + i) + }; + ++msg->options.number; + } + } + + tpakManager.setRAMEnabled(false); + + // the last option in the option menu should be the Cancel option. + msg->options.items[msg->options.number] = { + .title = "Cancel", + .onConfirmAction = advanceDialog, + .context = context + }; + ++msg->options.number; + + setDialogDataText(*msg, "Which move should be forgotten?"); + scene->showDialog(msg); +} + +void gen1MoveDeleterApply(void* context, const void* param) +{ + Gen1TrainerPokemon poke; + DialogData* msg1 = nullptr; + DialogData* msg2 = nullptr; + MenuScene* scene = static_cast(context); + const MoveDeleterParams* deleteParams = (const MoveDeleterParams*)param; + Move previousMove = Move::POUND; // dummy value to avoid uninitialized compiler warning + const char* pokeName; + + TransferPakManager& tpakManager = scene->getDependencies().tpakManager; + TransferPakRomReader romReader(tpakManager); + TransferPakSaveManager saveManager(tpakManager); + const Gen1GameType gameType = static_cast(scene->getDependencies().specificGenVersion); + const Gen1LocalizationLanguage language = static_cast(scene->getDependencies().localization); + Gen1GameReader gameReader(romReader, saveManager, gameType, language); + + tpakManager.setRAMEnabled(true); + + Gen1Party party = gameReader.getParty(); + + party.getPokemon(deleteParams->partyIndex, poke, true); + + switch(deleteParams->moveIndex) + { + case 0: + previousMove = (Move)poke.index_move1; + break; + case 1: + previousMove = (Move)poke.index_move2; + break; + case 2: + previousMove = (Move)poke.index_move3; + break; + case 3: + previousMove = (Move)poke.index_move4; + break; + } + + // apparently the Gen I games can't deal with having a move removed in between 2 moves. + // They only accept moves deleted at the end of the list. + // So when deleting a move, we need to shift every remaining move accordingly. + if(deleteParams->moveIndex < 1) + { + poke.index_move1 = poke.index_move2; + poke.pp_move1 = poke.pp_move2; + } + + if(deleteParams->moveIndex < 2) + { + poke.index_move2 = poke.index_move3; + poke.pp_move2 = poke.pp_move3; + } + if(deleteParams->moveIndex < 3) + { + poke.index_move3 = poke.index_move4; + poke.pp_move3 = poke.pp_move4; + } + + poke.index_move4 = 0; + poke.pp_move4 = 0; + + if(!party.setPokemon(deleteParams->partyIndex, poke)) + { + tpakManager.setRAMEnabled(false); + debugf("%s: ERROR: can't update Pokémon at partyIndex %hu\r\n", __FUNCTION__, deleteParams->partyIndex); + return; + } + + gameReader.updateMainChecksum(); + tpakManager.finishWrites(); + + pokeName = party.getPokemonNickname(deleteParams->partyIndex); + + tpakManager.setRAMEnabled(false); + + msg2 = new DialogData{ + .shouldDeleteWhenDone = true + }; + msg1 = new DialogData{ + .next = msg2, + .shouldDeleteWhenDone = true + }; + + setDialogDataText(*msg1, "1..2..3..Poof!"); + setDialogDataText(*msg2, "%s forgot %s!", pokeName, getMoveString(static_cast(previousMove))); + + scene->showDialog(msg1); +} + void gen2ReceiveGSBall(void* context, const void* param) { MenuScene* scene = static_cast(context); TransferPakManager& tpakManager = scene->getDependencies().tpakManager; TransferPakRomReader romReader(tpakManager); TransferPakSaveManager saveManager(tpakManager); - Gen2GameReader gameReader(romReader, saveManager, Gen2GameType::CRYSTAL); + const Gen2LocalizationLanguage language = static_cast(scene->getDependencies().localization); + Gen2GameReader gameReader(romReader, saveManager, Gen2GameType::CRYSTAL, language); DialogData* messageData = new DialogData{ .shouldDeleteWhenDone = true }; @@ -434,7 +718,8 @@ void gen2SetEventFlag(void* context, const void* param) TransferPakManager& tpakManager = scene->getDependencies().tpakManager; TransferPakRomReader romReader(tpakManager); TransferPakSaveManager saveManager(tpakManager); - Gen2GameReader gameReader(romReader, saveManager, static_cast(scene->getDependencies().specificGenVersion)); + const Gen2LocalizationLanguage language = static_cast(scene->getDependencies().localization); + Gen2GameReader gameReader(romReader, saveManager, static_cast(scene->getDependencies().specificGenVersion), language); DialogData* messageData = new DialogData{ .shouldDeleteWhenDone = true }; @@ -492,13 +777,6 @@ void askConfirmationWipeSave(void* context, const void* param) void resetRTC(void* context, const void* param) { - // The game checks bit 7 on the sRTCStatusFlags field in SRAM - // this is set when the game detects wrong RTC register values. - // In order to let the game prompt to reconfigure the RTC clock, we just have to set this bit - // Based on sRTCStatusFlags, RecordRTCStatus, .set_bit_7 in - // https://github.com/pret/pokecrystal - // https://github.com/pret/pokegold - const uint8_t rtcStatusFieldValue = 0xC0; MenuScene* scene = static_cast(context); auto diag = new DialogData{ @@ -512,14 +790,17 @@ void resetRTC(void* context, const void* param) return; } + const Gen2GameType gameType = static_cast(scene->getDependencies().specificGenVersion); + const Gen2LocalizationLanguage language = static_cast(scene->getDependencies().localization); TransferPakManager& tpakManager = scene->getDependencies().tpakManager; + TransferPakRomReader romReader(tpakManager); + TransferPakSaveManager saveManager(tpakManager); + Gen2GameReader gameReader(romReader, saveManager, gameType, language); tpakManager.setRAMEnabled(true); - - tpakManager.switchGBSRAMBank(0); - - tpakManager.writeSRAM(0xC60, &rtcStatusFieldValue, 1); + gameReader.resetRTC(); tpakManager.finishWrites(); + tpakManager.setRAMEnabled(false); setDialogDataText(*diag, "The games' clock was reset! Start the game to reconfigure it! Don't forget to save!"); scene->showDialog(diag); diff --git a/src/scenes/AboutScene.cpp b/src/scenes/AboutScene.cpp index 29f5744..3110ea4 100644 --- a/src/scenes/AboutScene.cpp +++ b/src/scenes/AboutScene.cpp @@ -22,16 +22,22 @@ static const Rectangle imgGBDevIoBounds = {28, 280, 34, 55}; static const Rectangle gbDevIOTextBounds = {68, 300, 75, 16}; static const Rectangle imgNESDevWikiBounds = {163, 280, 96, 60}; static const Rectangle imgPokeTransporterGBBounds = {96, 350, 128, 39}; -static const Rectangle otherCreditsTextBounds = {5, 400, 310, 80}; +static const Rectangle otherCreditsTextBounds = {5, 400, 310, 160}; static const char* otherCreditsString = R"delim(github.com/magical/pokemon-sprites-rby glitchcity.wiki github.com/seanmorris/pokemon-parser github.com/LinusU/pokemon-sprite-compression github.com/xvillaneau/poke-sprite-python + +TESTING/VALIDATION + +/u/ImranFZakhaev +/u/bluemooncinco +MajorUpgrade )delim"; -static const char* headerTextString = R"delim(PokeMe64 Version 0.2 +static const char* headerTextString = R"delim(PokeMe64 Version 0.3 by risingPhil SPECIAL THANKS TO: @@ -40,8 +46,8 @@ SPECIAL THANKS TO: AboutScene::AboutScene(SceneDependencies& deps, void*) : AbstractUIScene(deps) - , fontIdArial_(1) - , fontArialStyleWhiteId_(0) + , fontIdMainFont_(1) + , fontMainFontStyleWhiteId_(0) , logoLibDragon_(nullptr) , logoBulbagarden_(nullptr) , logoRetroGameMechanicsExplained_(nullptr) @@ -70,14 +76,14 @@ AboutScene::AboutScene(SceneDependencies& deps, void*) }) , bButtonPressed_(false) { - fontIdArial_ = deps.fontManager.getFont("rom://Arial.font64"); + fontIdMainFont_ = deps.fontManager.getFont("rom://Arial.font64"); - const rdpq_fontstyle_t arialWhite = { + const rdpq_fontstyle_t mainFontWhite = { .color = RGBA32(0xFF, 0xFF, 0xFF, 0xFF), .outline_color = RGBA32(0, 0, 0, 0xFF) }; - deps.fontManager.registerFontStyle(fontIdArial_, fontArialStyleWhiteId_, arialWhite); + deps.fontManager.registerFontStyle(fontIdMainFont_, fontMainFontStyleWhiteId_, mainFontWhite); } AboutScene::~AboutScene() @@ -99,8 +105,8 @@ void AboutScene::init() const TextWidgetStyle headerTextStyle = { .renderSettingsNotFocused = { - .fontId = fontIdArial_, - .fontStyleId = fontArialStyleWhiteId_, + .fontId = fontIdMainFont_, + .fontStyleId = fontMainFontStyleWhiteId_, .halign = ALIGN_CENTER } // ,.backgroundColor_ = 0xF801 @@ -108,15 +114,15 @@ void AboutScene::init() const TextWidgetStyle commonLeftAlignedTextStyle = { .renderSettingsNotFocused = { - .fontId = fontIdArial_, - .fontStyleId = fontArialStyleWhiteId_, + .fontId = fontIdMainFont_, + .fontStyleId = fontMainFontStyleWhiteId_, .halign = ALIGN_LEFT } }; const TextWidgetStyle commonCenterAlignedTextStyle = { .renderSettingsNotFocused = { - .fontId = fontIdArial_, - .fontStyleId = fontArialStyleWhiteId_, + .fontId = fontIdMainFont_, + .fontStyleId = fontMainFontStyleWhiteId_, .halign = ALIGN_CENTER } }; diff --git a/src/scenes/DataCopyScene.cpp b/src/scenes/DataCopyScene.cpp index 59a12c7..fb1bc48 100644 --- a/src/scenes/DataCopyScene.cpp +++ b/src/scenes/DataCopyScene.cpp @@ -52,6 +52,9 @@ static void generateRomTitle(char* outputPath, const gameboy_cartridge_header& g case Gen1GameType::RED: strcpy(outputPath, "Red"); break; + case Gen1GameType::GREEN: + strcpy(outputPath, "Green"); + break; case Gen1GameType::YELLOW: strcpy(outputPath, "Yellow"); break; @@ -178,7 +181,7 @@ void DataCopyScene::init() case DataCopyOperation::BACKUP_SAVE: generateSaveFileName(savOutputPath, sizeof(savOutputPath), gameTitle, deps_.playerName); copySource_ = new TransferPakSaveManagerCopySource(saveManager_); - copyDestination_ = new TransferPakFileCopyDestination(savOutputPath); + copyDestination_ = new TransferPakFileCopyDestination(savOutputPath, (deps_.generation == 2)); totalBytesToCopy_ = convertSRAMSizeIntoNumBytes(gbHeader.ram_size_code); setDialogDataText(*msg2, "The save was backed up to %s!", savOutputPath); break; diff --git a/src/scenes/DistributionPokemonListScene.cpp b/src/scenes/DistributionPokemonListScene.cpp index 01353da..2164b5c 100755 --- a/src/scenes/DistributionPokemonListScene.cpp +++ b/src/scenes/DistributionPokemonListScene.cpp @@ -22,8 +22,8 @@ DistributionPokemonListScene::DistributionPokemonListScene(SceneDependencies& de : MenuScene(deps, context) , romReader_(deps.tpakManager) , saveManager_(deps.tpakManager) - , gen1Reader_(romReader_, saveManager_, static_cast(deps.specificGenVersion)) - , gen2Reader_(romReader_, saveManager_, static_cast(deps.specificGenVersion)) + , gen1Reader_(romReader_, saveManager_, static_cast(deps.specificGenVersion), static_cast(deps.localization)) + , gen2Reader_(romReader_, saveManager_, static_cast(deps.specificGenVersion), static_cast(deps.localization)) , iconFactory_(romReader_) , customListFiller_(menuList_) , diag_() @@ -199,11 +199,11 @@ void DistributionPokemonListScene::setupMenu() .bounds = {0, 1, 20, 20} }, .titleNotFocused = { - .fontId = arialId_, + .fontId = mainFontId_, .fontStyleId = fontStyleWhiteId_ }, .titleFocused = { - .fontId = arialId_, + .fontId = mainFontId_, .fontStyleId = fontStyleYellowId_ }, .leftMargin = 24, @@ -286,6 +286,7 @@ void DistributionPokemonListScene::loadDistributionPokemonList() .iconFactory = &iconFactory_, .generation = deps_.generation, .specificGenVersion = deps_.specificGenVersion, + .localization = deps_.localization, .iconType = (uint8_t)gen1Reader_.getPokemonIconType(gen1List[i]->poke.poke_index) }; } @@ -313,6 +314,7 @@ void DistributionPokemonListScene::loadDistributionPokemonList() .iconFactory = &iconFactory_, .generation = deps_.generation, .specificGenVersion = deps_.specificGenVersion, + .localization = deps_.localization, .iconType = iconType }; } diff --git a/src/scenes/InitTransferPakScene.cpp b/src/scenes/InitTransferPakScene.cpp index f9e656c..872ca8a 100755 --- a/src/scenes/InitTransferPakScene.cpp +++ b/src/scenes/InitTransferPakScene.cpp @@ -8,8 +8,11 @@ #include "gen2/Gen2GameReader.h" #include "menu/MenuEntries.h" +#include + static const Rectangle tpakDetectWidgetBounds = {60, 44, 200, 116}; + static void dialogFinishedCallback(void* context) { InitTransferPakScene* scene = (InitTransferPakScene*)context; @@ -41,6 +44,7 @@ InitTransferPakScene::~InitTransferPakScene() void InitTransferPakScene::init() { + uint8_t systemEntropy[4]; menu9SliceSprite_ = sprite_load("rom://menu-bg-9slice.sprite"); SceneWithDialogWidget::init(); @@ -49,10 +53,18 @@ void InitTransferPakScene::init() setFocusChain(&tpakDetectWidgetSegment_); pokeMe64TextSettings_ = TextRenderSettings{ - .fontId = arialId_, + .fontId = mainFontId_, .fontStyleId = fontStyleWhiteId_, .halign = ALIGN_CENTER }; + + // add some entropy for our rand() function. + // we'll apply this later. Right before we go to the main menu. + getentropy(systemEntropy, sizeof(systemEntropy)); + for(uint8_t i=0; i < sizeof(systemEntropy); ++i) + { + randomSeed_ += static_cast(systemEntropy[i]) << (i * 8); + } } void InitTransferPakScene::destroy() @@ -65,7 +77,7 @@ void InitTransferPakScene::destroy() void InitTransferPakScene::render(RDPQGraphics& gfx, const Rectangle& sceneBounds) { - gfx.drawText(Rectangle{0, 10, 320, 16}, "PokeMe64 by risingPhil. Version 0.2", pokeMe64TextSettings_); + gfx.drawText(Rectangle{0, 10, 320, 16}, "PokeMe64 by risingPhil. Version 0.3", pokeMe64TextSettings_); tpakDetectWidget_.render(gfx, sceneBounds); SceneWithDialogWidget::render(gfx, sceneBounds); @@ -124,6 +136,11 @@ void InitTransferPakScene::onDialogDone() return; } + // add the number of ticks it took before we got to this point to our random seed + randomSeed_ += static_cast(get_ticks()); + // Now apply random seed to srand() before we get to the main menu. + srand(randomSeed_); + deps_.sceneManager.switchScene(SceneType::MENU, deleteMenuSceneContext, menuContext); } @@ -185,7 +202,7 @@ void InitTransferPakScene::setupTPakDetectWidget() { const TransferPakDetectionWidgetStyle style = { .textSettings = { - .fontId = arialId_, + .fontId = mainFontId_, .fontStyleId = fontStyleWhiteId_, .halign = ALIGN_CENTER } @@ -229,6 +246,9 @@ void InitTransferPakScene::loadGameType() case Gen1GameType::RED: gameTypeString_ = "Red"; break; + case Gen1GameType::GREEN: + gameTypeString_ = "Green"; + break; case Gen1GameType::YELLOW: gameTypeString_ = "Yellow"; break; @@ -266,19 +286,43 @@ void InitTransferPakScene::loadSaveMetadata() TransferPakSaveManager saveManager(deps_.tpakManager); Gen1GameType gen1Type; Gen2GameType gen2Type; + uint16_t trainerID = 0; tpakDetectWidget_.retrieveGameType(gen1Type, gen2Type); if(gen1Type != Gen1GameType::INVALID) { - Gen1GameReader gameReader(romReader, saveManager, gen1Type); + const Gen1LocalizationLanguage language = gen1_determineGameLanguage(romReader, gen1Type); + + Gen1GameReader gameReader(romReader, saveManager, gen1Type, language); const char* trainerName = gameReader.getTrainerName(); + trainerID = gameReader.getTrainerID(); + deps_.localization = static_cast(language); strncpy(deps_.playerName, trainerName, sizeof(deps_.playerName) - 1); } else if(gen2Type != Gen2GameType::INVALID) { - Gen2GameReader gameReader(romReader, saveManager, gen2Type); + const Gen2LocalizationLanguage language = gen2_determineGameLanguage(romReader, gen2Type); + + Gen2GameReader gameReader(romReader, saveManager, gen2Type, language); const char* trainerName = gameReader.getTrainerName(); - strncpy(deps_.playerName, trainerName, sizeof(deps_.playerName) - 1); + trainerID = gameReader.getTrainerID(); + deps_.localization = static_cast(language); + if(language != Gen2LocalizationLanguage::KOREAN) + { + strncpy(deps_.playerName, trainerName, sizeof(deps_.playerName) - 1); + } + else + { + const char fakePlayerName[] = "Trainer"; + memcpy(deps_.playerName, fakePlayerName, sizeof(fakePlayerName)); + } + } + + // add to our random seed to make sure our random numbers will be randomized. + randomSeed_ += trainerID; + for(uint8_t i=0; i < strlen(deps_.playerName); ++i) + { + randomSeed_ += deps_.playerName[i]; } } diff --git a/src/scenes/MenuScene.cpp b/src/scenes/MenuScene.cpp index 6cea3b4..f1b7a83 100755 --- a/src/scenes/MenuScene.cpp +++ b/src/scenes/MenuScene.cpp @@ -202,11 +202,11 @@ void MenuScene::setupMenu() const MenuItemStyle itemStyle = { .size = {160, 16}, .titleNotFocused = { - .fontId = arialId_, + .fontId = mainFontId_, .fontStyleId = fontStyleWhiteId_ }, .titleFocused = { - .fontId = arialId_, + .fontId = mainFontId_, .fontStyleId = fontStyleYellowId_ }, .leftMargin = 35, diff --git a/src/scenes/PokeTransporterGBRefScene.cpp b/src/scenes/PokeTransporterGBRefScene.cpp index 6342dfc..5153b28 100644 --- a/src/scenes/PokeTransporterGBRefScene.cpp +++ b/src/scenes/PokeTransporterGBRefScene.cpp @@ -20,8 +20,8 @@ PokeTransporterGBRefScene::PokeTransporterGBRefScene(SceneDependencies& deps, vo , pokeTransporterGBLogoSprite_(nullptr) , qrCodeSprite_(nullptr) , fennelPictureSprite_(nullptr) - , fontArialSmallId_(0) - , fontArialSmallWhiteId_(0) + , fontMainFontSmallId_(0) + , fontMainFontSmallWhiteId_(0) { } @@ -35,13 +35,13 @@ void PokeTransporterGBRefScene::init() pokeTransporterGBLogoSprite_ = sprite_load("rom://logo-poketransporter-gb.sprite"); qrCodeSprite_ = sprite_load("rom://qrcode-poketransporter-gb.sprite"); fennelPictureSprite_ = sprite_load("rom://fennel-picture.sprite"); - fontArialSmallId_ = deps_.fontManager.getFont("rom://Arial-small.font64"); + fontMainFontSmallId_ = deps_.fontManager.getFont("rom://Arial-small.font64"); - const rdpq_fontstyle_t arialWhite = { + const rdpq_fontstyle_t mainFontWhite = { .color = RGBA32(0xFF, 0xFF, 0xFF, 0xFF), .outline_color = RGBA32(0, 0, 0, 0xFF) }; - deps_.fontManager.registerFontStyle(fontArialSmallId_, fontArialSmallWhiteId_, arialWhite); + deps_.fontManager.registerFontStyle(fontMainFontSmallId_, fontMainFontSmallWhiteId_, mainFontWhite); SceneWithDialogWidget::init(); @@ -80,8 +80,8 @@ void PokeTransporterGBRefScene::render(RDPQGraphics& gfx, const Rectangle& scene .renderMode = SpriteRenderMode::NORMAL }; TextRenderSettings textSettings = { - .fontId = fontArialSmallId_, - .fontStyleId = fontArialSmallWhiteId_ + .fontId = fontMainFontSmallId_, + .fontStyleId = fontMainFontSmallWhiteId_ }; gfx.drawSprite(logoRectangle, pokeTransporterGBLogoSprite_, renderSettings); diff --git a/src/scenes/SceneWithDialogWidget.cpp b/src/scenes/SceneWithDialogWidget.cpp index 9c3fc97..6de962b 100755 --- a/src/scenes/SceneWithDialogWidget.cpp +++ b/src/scenes/SceneWithDialogWidget.cpp @@ -8,7 +8,7 @@ SceneWithDialogWidget::SceneWithDialogWidget(SceneDependencies& deps) , dialogFocusChainSegment_({ .current = &dialogWidget_ }) - , arialId_(1) + , mainFontId_(1) , fontStyleWhiteId_(0) , fontStyleYellowId_(1) { @@ -26,11 +26,11 @@ void SceneWithDialogWidget::init() .style = { .size = {140, 16}, .titleNotFocused = { - .fontId = arialId_, + .fontId = mainFontId_, .fontStyleId = fontStyleWhiteId_ }, .titleFocused = { - .fontId = arialId_, + .fontId = mainFontId_, .fontStyleId = fontStyleYellowId_ }, .leftMargin = 5, @@ -38,7 +38,7 @@ void SceneWithDialogWidget::init() } }, .textSettings = { - .fontId = arialId_, + .fontId = mainFontId_, .fontStyleId = fontStyleWhiteId_ }, .margin = { @@ -81,19 +81,19 @@ void SceneWithDialogWidget::showDialog(DialogData* diagData) void SceneWithDialogWidget::setupFonts() { - arialId_ = deps_.fontManager.getFont("rom://Arial.font64"); + mainFontId_ = deps_.fontManager.getFont("rom://Arial.font64"); - const rdpq_fontstyle_t arialWhite = { + const rdpq_fontstyle_t mainFontWhite = { .color = RGBA32(0xFF, 0xFF, 0xFF, 0xFF), .outline_color = RGBA32(0, 0, 0, 0xFF) }; - const rdpq_fontstyle_t arialYellow = { + const rdpq_fontstyle_t mainFontYellow = { .color = RGBA32(0xFF, 0xFF, 0x00, 0xFF), .outline_color = RGBA32(0, 0, 0, 0xFF) }; - deps_.fontManager.registerFontStyle(arialId_, fontStyleWhiteId_, arialWhite); - deps_.fontManager.registerFontStyle(arialId_, fontStyleYellowId_, arialYellow); + deps_.fontManager.registerFontStyle(mainFontId_, fontStyleWhiteId_, mainFontWhite); + deps_.fontManager.registerFontStyle(mainFontId_, fontStyleYellowId_, mainFontYellow); } void SceneWithDialogWidget::setupDialog(DialogWidgetStyle& style) diff --git a/src/scenes/SceneWithProgressBar.cpp b/src/scenes/SceneWithProgressBar.cpp index 7403ef4..9221eb9 100644 --- a/src/scenes/SceneWithProgressBar.cpp +++ b/src/scenes/SceneWithProgressBar.cpp @@ -40,7 +40,7 @@ void SceneWithProgressBar::setupProgressBar(ProgressBarWidgetStyle& style) .color = RGBA32(0x0, 0x61, 0xFF, 0xFF) }; style.textSettings = { - .fontId = arialId_, + .fontId = mainFontId_, .fontStyleId = fontStyleWhiteId_, .halign = ALIGN_CENTER, .valign = VALIGN_CENTER diff --git a/src/scenes/SelectFileScene.cpp b/src/scenes/SelectFileScene.cpp index 51dbf88..3260346 100755 --- a/src/scenes/SelectFileScene.cpp +++ b/src/scenes/SelectFileScene.cpp @@ -78,11 +78,11 @@ void SelectFileScene::init() .spriteBounds = { 5, 2, 16, 16} }, .titleNotFocused = { - .fontId = arialId_, + .fontId = mainFontId_, .fontStyleId = fontStyleWhiteId_ }, .titleFocused = { - .fontId = arialId_, + .fontId = mainFontId_, .fontStyleId = fontStyleYellowId_ }, .leftMargin = 24, @@ -187,7 +187,7 @@ void SelectFileScene::render(RDPQGraphics& gfx, const Rectangle& sceneBounds) if(context_->titleText && !isZeroSizeRectangle(titleBounds)) { const TextRenderSettings renderSettings = { - .fontId = arialId_, + .fontId = mainFontId_, .fontStyleId = fontStyleWhiteId_, .halign = ALIGN_CENTER }; diff --git a/src/scenes/StatsScene.cpp b/src/scenes/StatsScene.cpp index 9835529..b1d7bff 100644 --- a/src/scenes/StatsScene.cpp +++ b/src/scenes/StatsScene.cpp @@ -59,25 +59,25 @@ StatsScene::StatsScene(SceneDependencies& deps, void* context) , diag_({0}) , romReader_(deps.tpakManager) , saveManager_(deps.tpakManager) - , gen1GameReader_(romReader_, saveManager_, getGen1TypeFor(deps.generation, deps.specificGenVersion)) - , gen2GameReader_(romReader_, saveManager_, getGen2TypeFor(deps.generation, deps.specificGenVersion)) + , gen1GameReader_(romReader_, saveManager_, getGen1TypeFor(deps.generation, deps.specificGenVersion), static_cast(deps.localization)) + , gen2GameReader_(romReader_, saveManager_, getGen2TypeFor(deps.generation, deps.specificGenVersion), static_cast(deps.localization)) , menu9SliceSprite_(nullptr) , backgroundRenderSettings_({ .renderMode = SpriteRenderMode::NINESLICE, .srcRect = Rectangle{6, 6, 6, 6} }) - , fontArialSmallId_(2) - , fontArialSmallWhiteId_(0) + , fontMainFontSmallId_(2) + , fontMainFontSmallWhiteId_(0) , textSettings_(TextRenderSettings{ - .fontId = arialId_, + .fontId = mainFontId_, .fontStyleId = fontStyleWhiteId_ }) , smallTextSettings_(TextRenderSettings{ - .fontId = fontArialSmallId_, - .fontStyleId = fontArialSmallWhiteId_ + .fontId = fontMainFontSmallId_, + .fontStyleId = fontMainFontSmallWhiteId_ }) , statsSettings_(TextRenderSettings{ - .fontId = arialId_, + .fontId = mainFontId_, .fontStyleId = fontStyleWhiteId_, }) , spriteBounds_(spriteBounds) @@ -118,16 +118,16 @@ void StatsScene::init() bool shiny; menu9SliceSprite_ = sprite_load("rom://menu-bg-9slice.sprite"); - fontArialSmallId_ = deps_.fontManager.getFont("rom://Arial-small.font64"); + fontMainFontSmallId_ = deps_.fontManager.getFont("rom://Arial-small.font64"); trainerName = deps_.playerName; SceneWithDialogWidget::init(); - const rdpq_fontstyle_t arialWhite = { + const rdpq_fontstyle_t mainFontWhite = { .color = RGBA32(0xFF, 0xFF, 0xFF, 0xFF), .outline_color = RGBA32(0, 0, 0, 0xFF) }; - deps_.fontManager.registerFontStyle(fontArialSmallId_, fontArialSmallWhiteId_, arialWhite); + deps_.fontManager.registerFontStyle(fontMainFontSmallId_, fontMainFontSmallWhiteId_, mainFontWhite); deps_.tpakManager.setRAMEnabled(true); switch(deps_.generation) { @@ -166,7 +166,7 @@ void StatsScene::init() move2Str = getMoveString(static_cast(context_->poke_g2.index_move2)); move3Str = getMoveString(static_cast(context_->poke_g2.index_move3)); move4Str = getMoveString(static_cast(context_->poke_g2.index_move4)); - pokeName = gen2GameReader_.getPokemonName(pokeIndex); + pokeName = (deps_.localization != (uint8_t)Gen2LocalizationLanguage::KOREAN) ? gen2GameReader_.getPokemonName(pokeIndex) : "Pokémon"; shiny = gen2_isPokemonShiny(context_->poke_g2); snprintf(pokeStatsString_, sizeof(pokeStatsString_), "ATK: %u\nDEF: %u\nSPEC. ATK: %u\nSPEC. DEF: %u\nSPEED: %u", atk, def, specAtk, specDef, speed); break; diff --git a/src/scenes/TestScene.cpp b/src/scenes/TestScene.cpp index 6141393..3972c41 100755 --- a/src/scenes/TestScene.cpp +++ b/src/scenes/TestScene.cpp @@ -53,11 +53,11 @@ void TestScene::init() .itemStyle = { .size = {280, 16}, .titleNotFocused = { - .fontId = arialId_, + .fontId = mainFontId_, .fontStyleId = fontStyleWhiteId_ }, .titleFocused = { - .fontId = arialId_, + .fontId = mainFontId_, .fontStyleId = fontStyleYellowId_ }, .leftMargin = 10, diff --git a/src/transferpak/TransferPakDataCopier.cpp b/src/transferpak/TransferPakDataCopier.cpp index c5c1e1a..b3ff47a 100644 --- a/src/transferpak/TransferPakDataCopier.cpp +++ b/src/transferpak/TransferPakDataCopier.cpp @@ -189,9 +189,10 @@ void TransferPakSaveManagerDestination::close() // dummy } -TransferPakFileCopyDestination::TransferPakFileCopyDestination(const char* pathOnSDCard) +TransferPakFileCopyDestination::TransferPakFileCopyDestination(const char* pathOnSDCard, bool resetRTC) : outputFile_(nullptr) , bytesWritten_(0) + , resetRTC_(resetRTC) { outputFile_ = fopen(pathOnSDCard, "w"); } @@ -225,11 +226,29 @@ uint32_t TransferPakFileCopyDestination::write(uint8_t* buffer, uint32_t bytesTo void TransferPakFileCopyDestination::close() { - if(outputFile_) + if(!outputFile_) { - fclose(outputFile_); - outputFile_ = nullptr; + return; } + + if(resetRTC_) + { + // The game checks bit 7 on the sRTCStatusFlags field in SRAM + // this is set when the game detects wrong RTC register values. + // In order to let the game prompt to reconfigure the RTC clock, we just have to set this bit + // Based on sRTCStatusFlags, RecordRTCStatus, .set_bit_7 in + // https://github.com/pret/pokecrystal + // https://github.com/pret/pokegold + const uint8_t rtcStatusFieldValue = 0xC0; + if(fseek(outputFile_, 0xC60, SEEK_SET) == 0) + { + // seek successful + fwrite(&rtcStatusFieldValue, 1, 1, outputFile_); + } + } + + fclose(outputFile_); + outputFile_ = nullptr; } TransferPakDataCopier::TransferPakDataCopier(ITransferPakDataCopySource& source, ITransferPakDataCopyDestination& destination) diff --git a/src/transferpak/TransferPakManager.cpp b/src/transferpak/TransferPakManager.cpp index da8f629..23fd11c 100755 --- a/src/transferpak/TransferPakManager.cpp +++ b/src/transferpak/TransferPakManager.cpp @@ -180,10 +180,24 @@ void TransferPakManager::switchGBSRAMBank(uint8_t bankIndex) writeBufferSRAMBankOffset_ = 0xFFFF; } +void TransferPakManager::switchMBC1BankingMode(uint8_t mode) +{ + uint8_t data[TPAK_BLOCK_SIZE]; + + debugf("[TransferPakManager]: %s(%hhu)\r\n", __FUNCTION__, mode); + // make sure to finish any writes in the write buffer before switching + finishWrites(); + + memset(data, mode, TPAK_BLOCK_SIZE); + tpak_write(port_, 0x6000, data, TPAK_BLOCK_SIZE); + + // invalidate read and write buffer + readBufferBankOffset_ = 0xFFFF; + writeBufferSRAMBankOffset_ = 0xFFFF; +} + void TransferPakManager::read(uint16_t gbAddress, uint8_t* data, uint16_t size) { -// debugf("[TransferPakManager]: %s(0x%x, %p, %u)\r\n", __FUNCTION__, gbAddress, data, size); - uint16_t bytesRemaining = size; uint8_t* cur = data; diff --git a/src/widget/PokemonPartyIconWidget.cpp b/src/widget/PokemonPartyIconWidget.cpp index bf60d05..03abe8d 100644 --- a/src/widget/PokemonPartyIconWidget.cpp +++ b/src/widget/PokemonPartyIconWidget.cpp @@ -35,7 +35,7 @@ PokemonPartyIconFactory::~PokemonPartyIconFactory() } } -surface_t PokemonPartyIconFactory::getIcon(uint8_t generation, uint8_t specificGenType, uint8_t iconType, bool firstFrame) +surface_t PokemonPartyIconFactory::getIcon(uint8_t generation, uint8_t specificGenType, uint8_t localization, uint8_t iconType, bool firstFrame) { BufferBasedSaveManager dummy(nullptr, 0); SpriteRenderer spriteRenderer; @@ -53,7 +53,7 @@ surface_t PokemonPartyIconFactory::getIcon(uint8_t generation, uint8_t specificG if(generation == 1) { - Gen1GameReader gameReader(romReader_, dummy, (Gen1GameType)specificGenType); + Gen1GameReader gameReader(romReader_, dummy, static_cast(specificGenType), static_cast(localization)); iconWidthInPixels = GEN1_ICON_WIDTH_IN_TILES * 8; iconHeightInPixels = GEN1_ICON_HEIGHT_IN_TILES * 8; @@ -67,7 +67,7 @@ surface_t PokemonPartyIconFactory::getIcon(uint8_t generation, uint8_t specificG } else if(generation == 2) { - Gen2GameReader gameReader(romReader_, dummy, (Gen2GameType)specificGenType); + Gen2GameReader gameReader(romReader_, dummy, static_cast(specificGenType), static_cast(localization)); iconWidthInPixels = GEN2_ICON_WIDTH_IN_TILES * 8; iconHeightInPixels = GEN2_ICON_HEIGHT_IN_TILES * 8; @@ -147,8 +147,8 @@ void PokemonPartyIconWidget::setData(const PokemonPartyIconWidgetData& data) return; } - iconFrame1_ = data.iconFactory->getIcon(data_.generation, data_.specificGenVersion, data_.iconType, true); - iconFrame2_ = data.iconFactory->getIcon(data_.generation, data_.specificGenVersion, data_.iconType, false); + iconFrame1_ = data.iconFactory->getIcon(data_.generation, data_.specificGenVersion, data_.localization, data_.iconType, true); + iconFrame2_ = data.iconFactory->getIcon(data_.generation, data_.specificGenVersion, data_.localization, data_.iconType, false); reset(); } diff --git a/src/widget/TransferPakDetectionWidget.cpp b/src/widget/TransferPakDetectionWidget.cpp index 3d11a6f..3c88cfb 100755 --- a/src/widget/TransferPakDetectionWidget.cpp +++ b/src/widget/TransferPakDetectionWidget.cpp @@ -293,6 +293,20 @@ bool TransferPakDetectionWidget::validateGameboyHeader() return false; } + // For MBC1 cartridges, we need to set the MBC1 banking mode to 1. + // If we don't, we can't actually switch SRAM banks. + // It looks like only the Japanese cartridges use MBC1 though. + switch(cartridgeHeader.cartridge_type) + { + case GB_MBC1_RAM: + case GB_MBC1_RAM_BATTERY: + tpakManager_.switchMBC1BankingMode(1); + break; + default: + // We don't need to do anything + break; + } + return true; } @@ -331,6 +345,9 @@ void TransferPakDetectionWidget::updateCartridgeIcon() cartridgeIconRenderSettings_.customPalette.colorsRGBA16 = paletteYellow; labelSpritePath = "rom://cartridge-label-yellow.sprite"; break; + case Gen1GameType::GREEN: + labelSpritePath = "rom://cartridge-label-green.sprite"; + break; default: break; } diff --git a/src/widget/VerticalList.cpp b/src/widget/VerticalList.cpp index 57c1a36..4ad5c6f 100755 --- a/src/widget/VerticalList.cpp +++ b/src/widget/VerticalList.cpp @@ -247,6 +247,15 @@ void VerticalList::clearWidgets() widgetBoundsList_.clear(); focusedWidgetIndex_ = 0; windowMinY_ = 0; + + // when autogrowing, the list shifts upwards everytime an entry was added. + // but when all widgets are removed, the bounds should reset again. + if(listStyle_.autogrow.enabled) + { + bounds_.y += bounds_.height; + bounds_.height = 0; + } + notifyScrollWindowListeners(); }