From 711107d4536b8644d467b1b762e871d7d308da6d Mon Sep 17 00:00:00 2001 From: Philippe Symons Date: Wed, 11 Sep 2024 22:31:50 +0200 Subject: [PATCH] Feature/backup cartridge save to flashcart sd (#7) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merged feature/backup cartridge save to flashcart sd Summary: * Add menu options to backup/restore a save from a pokémon gen I/II cartridge to the N64 flashcarts' SD card. This is only compatible with 64Drive, Everdrive64, ED64Plus and SummerCart64! * Add menu option to wipe a save from a cartridge * Add menu option to reset the in-game clock of gen II cartridges. This will let the game prompt to update the clock on the next time you try to load the save file. Before it was quite hard to reset the game clock without starting a new save. Especially for crystal you had to calculate some kind of password to make it work. Not anymore. PokeMe64 now makes this easy! Commits: * Write a hello world of sorts for writing to SD card. This just writes a file sd://helloworld.txt to the sd card. It will form the basis for implementing save backup * Implement save file backup functionality. It works! But it needs some UI work (progress bar or something), because it takes > 5 seconds * Add ProgressBarWidget and SceneWithProgressBar * Add DataCopyScene and implement backup/restore with progress indication properly Added a new DataCopyScene which takes care of displaying a progress bar while we're backup'ing or restoring data. I also implemented cartridge Rom and Save backup to (micro)SD card and Save restore from (micro)SD card to the cartridge. And it works! I verified it with my Pokémon Blue cartridge: I can back up its save, play the game, release almost all pokémon, go to a different location and save again and then restore the previous save using PokeMe64 Rom backup also works: I can play the resulting rom in VisualBoyAdvance without any issue. I can also use the save that I backed up in VisualBoyAdvance. So right now, you can take an emulator save/pkhex save and transfer it to an actual (original) pokémon cartridge and it will work! However, it's not ready for release yet for a few reasons: - It will currently always output to sd:/gb_out.sav and sd:/rom.gb. I want it to output to sd:/_.sav and sd:/.gb instead. In fact I want PokeMe64 to add numbers to the save file in case one already exists with that name. - libdragon currently doesn't have the functionality to create directories. I wish PokeMe64 to backup to a directory called PokeMe64, even if the directory doesn't exist yet. - I want a file selector so you can select which save file to restore. This way you could have multiple saves. In theory that would also allow you to transfer a pokémon red save to a pokémon blue cartridge and so on. - I want to have a wipe save option - I need to safeguard PokeMe64 for corrupted save data. Right now, for reproduction carts PokeMe64 crashes while trying to decode the trainerName. This is because the actual save file is not accessible on a reproduction cart. If I'm going to allow people to restore random saves (even from different versions alltogether), I need PokeMe64 to be robust enough for people to be able to wipe the save file or restore a different one. (in order for them to be able to correct their mistake without having to take out the battery). In order to be robust enough, I should make sure to check the CRC checksum of the save file before going to the main menu. And if it doesn't match, I should offer a reduced menu to the user. So... no release of this feature until I resolve all the topics above. But man, it's so satisfying to see it work in the current form already! The functionality itself just works on first try! * Add FileBrowserWidget and rework TestScene to use it for testing purposes * Add SelectFileScene and connect everything together. * Generate unique save file names __.sav (the last part is optional) This allows you to backup your save multiple times without overwriting the pre-existing ones. * Add option to wipe save and validate save CRC before continuing to main menu If an invalid/corrupt save is found, the user will only be offered the backup/restore options The wipe save option exists in case the user messes up and restores a save that makes the gameboy crash on bootup. (I don't know if that can actually happen, but just in case) After all: the backup/restore options allow for restoring saves of different games than the cartridge you're restoring to. (a red save to a blue cartridge, ...) Therefore the user could also -accidentally- restore a gold save to a blue cartridge (for example). So yeah, that option is only there to fix theoretic accidents without the user having to take out the battery. * Add "Reset clock" function so you can easily reconfigure your game clock in gen 2 There's no straightforward in gen 2 to reconfigure the in-game clock. There's an arcane key combination that's the worst on Pokémon Crystal and requires you to calculate some kind of password. Now PokeMe64 makes it easy: the "Reset Clock" function sets a flag that will let the game prompt you in the main menu to reconfigure the date/time again. * Update README.md * Move Reset Clock option to the main menu * Some usability tweaks of the SelectFileScene - Add title - Add scroll arrows - Make it possible to go back to the previous scene - Increase PokeMe64 version to 0.2 * Ask confirmation before wiping the save --- README.md | 9 +- assets/bg-nineslice-transparant-border.png | Bin 0 -> 11085 bytes include/core/DragonUtils.h | 35 ++ include/core/Sprite.h | 1 + include/core/common.h | 20 ++ include/menu/MenuEntries.h | 3 + include/menu/MenuFunctions.h | 17 +- include/scenes/AbstractUIScene.h | 2 + include/scenes/DataCopyScene.h | 55 +++ include/scenes/IScene.h | 3 + include/scenes/InitTransferPakScene.h | 6 +- include/scenes/MenuScene.h | 2 - include/scenes/SceneWithProgressBar.h | 28 ++ include/scenes/SelectFileScene.h | 60 ++++ include/scenes/TestScene.h | 30 +- include/transferpak/TransferPakDataCopier.h | 198 +++++++++++ include/transferpak/TransferPakManager.h | 2 +- include/widget/DialogWidget.h | 4 + include/widget/FileBrowserWidget.h | 156 +++++++++ include/widget/ImageWidget.h | 2 +- include/widget/MenuItemWidget.h | 1 + include/widget/ProgressBarWidget.h | 109 ++++++ include/widget/TransferPakDetectionWidget.h | 8 +- libpokemegb | 2 +- src/core/Application.cpp | 2 + src/core/DragonUtils.cpp | 86 +++++ src/core/common.cpp | 27 ++ src/menu/MenuEntries.cpp | 44 +++ src/menu/MenuFunctions.cpp | 118 ++++++- src/scenes/AboutScene.cpp | 2 +- src/scenes/AbstractUIScene.cpp | 5 + src/scenes/DataCopyScene.cpp | 359 ++++++++++++++++++++ src/scenes/InitTransferPakScene.cpp | 108 ++++-- src/scenes/MenuScene.cpp | 5 - src/scenes/SceneManager.cpp | 10 +- src/scenes/SceneWithProgressBar.cpp | 50 +++ src/scenes/SelectFileScene.cpp | 220 ++++++++++++ src/scenes/StatsScene.cpp | 3 +- src/scenes/TestScene.cpp | 227 +++++-------- src/transferpak/TransferPakDataCopier.cpp | 276 +++++++++++++++ src/transferpak/TransferPakManager.cpp | 15 +- src/widget/FileBrowserWidget.cpp | 344 +++++++++++++++++++ src/widget/MenuItemWidget.cpp | 5 + src/widget/ProgressBarWidget.cpp | 117 +++++++ src/widget/TransferPakDetectionWidget.cpp | 78 +++-- src/widget/VerticalList.cpp | 5 +- 46 files changed, 2610 insertions(+), 249 deletions(-) create mode 100644 assets/bg-nineslice-transparant-border.png create mode 100644 include/scenes/DataCopyScene.h create mode 100644 include/scenes/SceneWithProgressBar.h create mode 100755 include/scenes/SelectFileScene.h create mode 100644 include/transferpak/TransferPakDataCopier.h create mode 100644 include/widget/FileBrowserWidget.h create mode 100644 include/widget/ProgressBarWidget.h create mode 100644 src/scenes/DataCopyScene.cpp create mode 100644 src/scenes/SceneWithProgressBar.cpp create mode 100755 src/scenes/SelectFileScene.cpp create mode 100644 src/transferpak/TransferPakDataCopier.cpp create mode 100644 src/widget/FileBrowserWidget.cpp create mode 100644 src/widget/ProgressBarWidget.cpp diff --git a/README.md b/README.md index 46f7db6..ed8115d 100755 --- a/README.md +++ b/README.md @@ -17,6 +17,10 @@ I'm happy to accept pull requests if the community wants to do them. - Teach Pikachu Surf/Fly on Gen 1 cartridges - 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. +- Make it possible to restore a save file on the N64 flashcart to an actual Pokémon gameboy cartridge. You can even restore emulator saves! +- Be able to wipe the save file from a game cartridge (mostly added as a feature in case you mess up and restore a save file from a completely different game) +- 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. @@ -34,7 +38,7 @@ To build it, set up a [build environment for libdragon](https://github.com/Drago # Usage WARNING: Do not insert or remove your gameboy cartridge, N64 transfer pak or controller while the Nintendo 64 is powered on. Doing so might corrupt your save file or just plainly won't work! (header validation check may fail) -- Copy PokeMe64.z64 to your Nintendo 64 flash cartridge. (such as Everdrive64, Super 64, ED64Plus, ...) +- Copy PokeMe64.z64 to your Nintendo 64 flash cartridge. (such as Everdrive64, Super 64, ED64Plus, SummerCart 64, ...) - Have your Nintendo 64 powered off. (IMPORTANT) - Connect your N64 transfer pak to your original (OEM) Nintendo controller. Third party controllers possibly don't work. You can verify this by testing with Pokémon Stadium 1 or 2 first. - Insert your pokémon gameboy cartridge into your N64 transfer Pak @@ -61,9 +65,8 @@ But having it done with a Nintendo 64 feels more "real"/"official" and is easier - Have a "music" widget that shows up to name the song(s) that I end up using when it/they start(s) playing. (similar to how [Need For Speed - Most Wanted (original)](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhWk37230YvbMHaMchN8dzQiRrO66VofThpcbvUTFMoplDbkQKBVUFcIabbNCnzZ0KpuxcAQmrXQjBlqv_bvi6v6xpjmPxs3tJ-ZI_GhOn3xe5DW7XpMbtnCKFcbBQ-l_zzbrIIV4smBpth/s1600/_mwmusic.jpg) used to show this) ## Feature ideas -- Support reproduction cartridges (in libpokemegb) +- Support reproduction cartridges - Support other language versions (in libpokemegb) -- Make it possible to backup your cartridge save file onto the flashcart PokeMe64 is running from. - 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) diff --git a/assets/bg-nineslice-transparant-border.png b/assets/bg-nineslice-transparant-border.png new file mode 100644 index 0000000000000000000000000000000000000000..73d157576ff8785cf20172a3055a8e3bd679d4d8 GIT binary patch literal 11085 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 vH)#Rp`&VE0S6^ZZOf~dVTh=C{VPF6@VZ=rSBO( typedef struct sprite_s sprite_t; diff --git a/include/core/common.h b/include/core/common.h index 9608691..d024696 100755 --- a/include/core/common.h +++ b/include/core/common.h @@ -30,6 +30,26 @@ typedef struct Rectangle int height; } Rectangle; +/** + * @brief This class wraps a raw char pointer. + * This is to allow for automatic memory management without all of the bloat + * of std::string. This is useful to pass it as part of a SceneContext and have + * the string automatically released when the context gets deleted. + */ +class ManagedString +{ +public: + ManagedString(char* rawString); + ~ManagedString(); + + const char* get() const; + + void operator = (char* rawString); +protected: +private: + char* rawString_; +}; + /** * Whether or not the rectangle has a size of 0. */ diff --git a/include/menu/MenuEntries.h b/include/menu/MenuEntries.h index d822972..4fd8dd7 100755 --- a/include/menu/MenuEntries.h +++ b/include/menu/MenuEntries.h @@ -15,4 +15,7 @@ extern const uint32_t gen2CrystalMenuEntriesSize; extern MenuItemData gen2DecorationMenuEntries[]; extern const uint32_t gen2DecorationMenuEntriesSize; +extern MenuItemData backupRestoreMenuEntries[]; +extern const uint32_t backupRestoreMenuEntriesSize; + #endif \ No newline at end of file diff --git a/include/menu/MenuFunctions.h b/include/menu/MenuFunctions.h index b021e3b..e0c84c0 100755 --- a/include/menu/MenuFunctions.h +++ b/include/menu/MenuFunctions.h @@ -1,14 +1,21 @@ #ifndef _MENUFUNCTIONS_H #define _MENUFUNCTIONS_H +#include "scenes/DataCopyScene.h" #include "Moves.h" #include -// these are used to pass as a pointer to the gen1PrepareToTeachPikachu +// these are used to pass as a pointer to the gen1PrepareToTeachPikachu function extern const Move MOVE_SURF; extern const Move MOVE_FLY; +// these are used to pass as a pointer to the goToDataCopyScene function +extern const DataCopyOperation DATACOPY_BACKUP_SAVE; +extern const DataCopyOperation DATACOPY_BACKUP_ROM; +extern const DataCopyOperation DATACOPY_RESTORE_SAVE; +extern const DataCopyOperation DATACOPY_WIPE_SAVE; + extern const uint16_t GEN2_EVENTFLAG_DECORATION_PIKACHU_BED; extern const uint16_t GEN2_EVENTFLAG_DECORATION_UNOWN_DOLL; extern const uint16_t GEN2_EVENTFLAG_DECORATION_TENTACOOL_DOLL; @@ -19,16 +26,24 @@ void advanceDialog(void* context, const void* param); 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 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); void gen1PrepareToTeachPikachu(void* context, const void* param); void gen1TeachPikachu(void* context, const void* param); void gen2ReceiveGSBall(void* context, const void* param); void gen2SetEventFlag(void* context, const void* param); +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 + */ +void resetRTC(void* context, const void* param); #endif \ No newline at end of file diff --git a/include/scenes/AbstractUIScene.h b/include/scenes/AbstractUIScene.h index 734c718..05e21dc 100755 --- a/include/scenes/AbstractUIScene.h +++ b/include/scenes/AbstractUIScene.h @@ -75,6 +75,8 @@ public: * So don't forget to set one! */ void setFocusChain(WidgetFocusChainSegment* focusChain); + + SceneDependencies& getDependencies(); protected: SceneDependencies& deps_; private: diff --git a/include/scenes/DataCopyScene.h b/include/scenes/DataCopyScene.h new file mode 100644 index 0000000..9f6a17f --- /dev/null +++ b/include/scenes/DataCopyScene.h @@ -0,0 +1,55 @@ +#ifndef _DATACOPYSCENE_H +#define _DATACOPYSCENE_H + +#include "scenes/SceneWithProgressBar.h" +#include "transferpak/TransferPakManager.h" +#include "transferpak/TransferPakRomReader.h" +#include "transferpak/TransferPakSaveManager.h" +#include "transferpak/TransferPakDataCopier.h" + +enum class DataCopyOperation +{ + BACKUP_SAVE, + BACKUP_ROM, + RESTORE_SAVE, + WIPE_SAVE +}; + +typedef struct DataCopySceneContext +{ + DataCopyOperation operation; + ManagedString saveToRestorePath; +} DataCopySceneContext; + +class DataCopyScene : public SceneWithProgressBar +{ +public: + DataCopyScene(SceneDependencies& deps, void* context); + virtual ~DataCopyScene(); + + void init() override; + void destroy() override; + + void processUserInput() override; + void render(RDPQGraphics& gfx, const Rectangle& sceneBounds) override; + + void onDialogDone(); +protected: + void setupDialog(DialogWidgetStyle& style) override; + void setupProgressBar(ProgressBarWidgetStyle& style) override; +private: + TransferPakRomReader romReader_; + TransferPakSaveManager saveManager_; + DataCopySceneContext* sceneContext_; + ITransferPakDataCopySource* copySource_; + ITransferPakDataCopyDestination* copyDestination_; + TransferPakDataCopier* copier_; + sprite_t* dialogWidgetSprite_; + sprite_t* progressBackgroundSprite_; + DialogData diag_; + uint32_t totalBytesToCopy_; +}; + +void deleteDataCopySceneContext(void* context); + +#endif \ No newline at end of file diff --git a/include/scenes/IScene.h b/include/scenes/IScene.h index f5198b4..78862b2 100755 --- a/include/scenes/IScene.h +++ b/include/scenes/IScene.h @@ -20,6 +20,8 @@ enum class SceneType STATS, TEST, POKETRANSPORTER_GB_REF, + SELECT_FILE, + COPY_DATA, ABOUT }; @@ -30,6 +32,7 @@ typedef struct SceneDependencies FontManager& fontManager; TransferPakManager& tpakManager; SceneManager& sceneManager; + char playerName[16]; uint8_t generation; uint8_t specificGenVersion; } SceneDependencies; diff --git a/include/scenes/InitTransferPakScene.h b/include/scenes/InitTransferPakScene.h index 2aab4de..0a6ccf1 100755 --- a/include/scenes/InitTransferPakScene.h +++ b/include/scenes/InitTransferPakScene.h @@ -4,8 +4,6 @@ #include "scenes/SceneWithDialogWidget.h" #include "widget/TransferPakDetectionWidget.h" -#define PLAYER_NAME_SIZE 15 - class TransferPakManager; /** @@ -30,7 +28,8 @@ private: void setupTPakDetectWidget(); void setupDialog(DialogWidgetStyle& style) override; - void loadGameMetadata(); + void loadGameType(); + void loadSaveMetadata(); const char* getGameTypeString(); sprite_t* menu9SliceSprite_; @@ -38,7 +37,6 @@ private: WidgetFocusChainSegment tpakDetectWidgetSegment_; DialogData diagData_; TextRenderSettings pokeMe64TextSettings_; - char playerName_[PLAYER_NAME_SIZE]; const char* gameTypeString_; }; diff --git a/include/scenes/MenuScene.h b/include/scenes/MenuScene.h index 53e7b16..8fa88c2 100755 --- a/include/scenes/MenuScene.h +++ b/include/scenes/MenuScene.h @@ -40,8 +40,6 @@ public: void focusChanged(const FocusChangeStatus& status) override; void onScrollWindowChanged(const ScrollWindowUpdate& update) override; - SceneDependencies& getDependencies(); - void showDialog(DialogData* diagData) override; protected: virtual void setupMenu(); diff --git a/include/scenes/SceneWithProgressBar.h b/include/scenes/SceneWithProgressBar.h new file mode 100644 index 0000000..770d0d1 --- /dev/null +++ b/include/scenes/SceneWithProgressBar.h @@ -0,0 +1,28 @@ +#ifndef _SCENEWITHPROGRESSBAR_H +#define _SCENEWITHPROGRESSBAR_H + +#include "scenes/SceneWithDialogWidget.h" +#include "widget/ProgressBarWidget.h" + +/** + * @brief This scene implementation adds a ProgressBarWidget on top of the + * SceneWithDialogWidget + */ +class SceneWithProgressBar : public SceneWithDialogWidget +{ +public: + SceneWithProgressBar(SceneDependencies& deps); + virtual ~SceneWithProgressBar(); + + void init() override; + + void render(RDPQGraphics& gfx, const Rectangle& sceneBounds) override; + + void setProgress(double progress); +protected: + virtual void setupProgressBar(ProgressBarWidgetStyle& style); +private: + ProgressBarWidget progressWidget_; +}; + +#endif \ No newline at end of file diff --git a/include/scenes/SelectFileScene.h b/include/scenes/SelectFileScene.h new file mode 100755 index 0000000..c2e30f2 --- /dev/null +++ b/include/scenes/SelectFileScene.h @@ -0,0 +1,60 @@ +#ifndef _SELECTFILESCENE_H +#define _SELECTFILESCENE_H + +#include "scenes/SceneWithDialogWidget.h" +#include "widget/FileBrowserWidget.h" +#include "widget/ImageWidget.h" + +typedef struct SelectFileSceneContext +{ + const char* titleText; + struct { + SceneType type; + void* context; + void (*deleteContextFunc)(void*); + } nextScene; + // initial path. If left NULL, it will default to sd:/ + const char* initialPath; + // file extension filter. if set, only files with the specified extension will be shown + const char* fileExtensionFilter; + // HACK: indicates that -instead of showing this scene- we want to navigate back to the previous scene instead + // this is useful for influencing back behaviour after the DataCopyScene is done. + // we want to end up back in the backup menu, so we need to go back twice in the scene history. + // This was the easiest option, because the main menu is game specific (and therefore much more difficult to go to directly) + // and so is the backup menu. + bool goBackToPreviousSceneInstead; +} SelectFileSceneContext; + +class SelectFileScene : public SceneWithDialogWidget +{ +public: + SelectFileScene(SceneDependencies& deps, void* sceneContext); + virtual ~SelectFileScene(); + + void init() override; + void destroy() override; + + bool handleUserInput(joypad_port_t port, const joypad_inputs_t& inputs) override; + void render(RDPQGraphics& gfx, const Rectangle& sceneBounds) override; + + void onDialogDone(); + + void onFileConfirmed(const char* path); + + void showDialog(DialogData* diagData) override; +protected: + void setupDialog(DialogWidgetStyle& style) override; +private: + FileBrowserWidget fileBrowser_; + WidgetFocusChainSegment fileBrowserFocusSegment_; + DialogData diag_; + SelectFileSceneContext* context_; + sprite_t* dialogWidgetBackgroundSprite_; + sprite_t* uiArrowUpSprite_; + sprite_t* uiArrowDownSprite_; + bool bButtonPressed_; +}; + +void deleteSelectFileSceneContext(void* context); + +#endif \ No newline at end of file diff --git a/include/scenes/TestScene.h b/include/scenes/TestScene.h index 712abd2..6235844 100755 --- a/include/scenes/TestScene.h +++ b/include/scenes/TestScene.h @@ -1,19 +1,11 @@ #ifndef _TESTSCENE_H #define _TESTSCENE_H -#include "scenes/AbstractUIScene.h" -#include "core/Sprite.h" -#include "widget/ScrollWidget.h" - -#include - -class ImageWidget; -class TextWidget; - -typedef std::vector WidgetList; +#include "scenes/SceneWithDialogWidget.h" +#include "widget/FileBrowserWidget.h" -class TestScene : public AbstractUIScene +class TestScene : public SceneWithDialogWidget { public: TestScene(SceneDependencies& deps, void* sceneContext); @@ -23,13 +15,19 @@ public: void destroy() override; void render(RDPQGraphics& gfx, const Rectangle& sceneBounds) override; + + void onDialogDone(); + + void onFileConfirmed(const char* path); + + void showDialog(DialogData* diagData) override; protected: + void setupDialog(DialogWidgetStyle& style) override; private: - ScrollWidget scrollWidget_; - WidgetFocusChainSegment scrollWidgetFocusSegment_; - WidgetList widgets_; - sprite_t* pokeballSprite_; - sprite_t* oakSprite_; + FileBrowserWidget fileBrowser_; + WidgetFocusChainSegment fileBrowserFocusSegment_; + DialogData diag_; + sprite_t* dialogWidgetBackgroundSprite_; }; #endif \ No newline at end of file diff --git a/include/transferpak/TransferPakDataCopier.h b/include/transferpak/TransferPakDataCopier.h new file mode 100644 index 0000000..503a3ec --- /dev/null +++ b/include/transferpak/TransferPakDataCopier.h @@ -0,0 +1,198 @@ +#ifndef _TRANSFERPAKDATACOPIER_H +#define _TRANSFERPAKDATACOPIER_H + +#include +#include + +class TransferPakRomReader; +class TransferPakSaveManager; + +/** + * This interface is used to define a transfer pak datasource to copy from + */ +class ITransferPakDataCopySource +{ +public: + virtual ~ITransferPakDataCopySource(); + + virtual bool readyForTransfer() const = 0; + + virtual uint16_t getCurrentBankIndex() const = 0; + virtual uint32_t getNumberOfBytesRead() const = 0; + + virtual uint32_t read(uint8_t *buffer, uint32_t bytesToRead) = 0; + +protected: +private: +}; + +/** + * This class implements the ITransferPakDataCopySource interface with TransferPakRomReader + */ +class TransferPakRomReaderCopySource : public ITransferPakDataCopySource +{ +public: + TransferPakRomReaderCopySource(TransferPakRomReader &romReader); + virtual ~TransferPakRomReaderCopySource(); + + bool readyForTransfer() const override; + + uint16_t getCurrentBankIndex() const override; + uint32_t getNumberOfBytesRead() const override; + + uint32_t read(uint8_t *buffer, uint32_t bytesToRead) override; + +protected: +private: + TransferPakRomReader &romReader_; + uint32_t bytesRead_; +}; + +/** + * This class implements the ITransferPakDataCopySource interface with TransferPakRomReader + */ +class TransferPakSaveManagerCopySource : public ITransferPakDataCopySource +{ +public: + TransferPakSaveManagerCopySource(TransferPakSaveManager &saveManager); + virtual ~TransferPakSaveManagerCopySource(); + + bool readyForTransfer() const override; + + uint16_t getCurrentBankIndex() const override; + uint32_t getNumberOfBytesRead() const override; + + uint32_t read(uint8_t *buffer, uint32_t bytesToRead) override; + +protected: +private: + TransferPakSaveManager &saveManager_; + uint32_t bytesRead_; +}; + +class TransferPakFileCopySource : public ITransferPakDataCopySource +{ +public: + TransferPakFileCopySource(const char *filePath); + virtual ~TransferPakFileCopySource(); + + bool readyForTransfer() const override; + + uint16_t getCurrentBankIndex() const override; + uint32_t getNumberOfBytesRead() const override; + + uint32_t read(uint8_t *buffer, uint32_t bytesToRead) override; + +protected: +private: + FILE *inputFile_; + uint32_t bytesRead_; +}; + +class TransferPakNullCopySource : public ITransferPakDataCopySource +{ +public: + TransferPakNullCopySource(); + virtual ~TransferPakNullCopySource(); + + bool readyForTransfer() const override; + + uint16_t getCurrentBankIndex() const override; + uint32_t getNumberOfBytesRead() const override; + + uint32_t read(uint8_t *buffer, uint32_t bytesToRead) override; +protected: +private: + uint32_t bytesRead_; +}; + +/** + * @brief This interface is used to define a transfer pak data destination to copy to + * + */ +class ITransferPakDataCopyDestination +{ +public: + virtual ~ITransferPakDataCopyDestination(); + + virtual bool readyForTransfer() const = 0; + + virtual uint16_t getCurrentBankIndex() const = 0; + virtual uint32_t getNumberOfBytesWritten() const = 0; + + virtual uint32_t write(uint8_t *buffer, uint32_t bytesToWrite) = 0; + + virtual void close() = 0; + +protected: +private: +}; + +class TransferPakSaveManagerDestination : public ITransferPakDataCopyDestination +{ +public: + TransferPakSaveManagerDestination(TransferPakSaveManager& saveManager); + virtual ~TransferPakSaveManagerDestination(); + + bool readyForTransfer() const override; + + uint16_t getCurrentBankIndex() const override; + uint32_t getNumberOfBytesWritten() const override; + + uint32_t write(uint8_t *buffer, uint32_t bytesToWrite) override; + + void close() override; +protected: +private: + TransferPakSaveManager& saveManager_; + uint32_t bytesWritten_; +}; + +class TransferPakFileCopyDestination : public ITransferPakDataCopyDestination +{ +public: + TransferPakFileCopyDestination(const char *pathOnSDCard); + virtual ~TransferPakFileCopyDestination(); + + bool readyForTransfer() const override; + + uint16_t getCurrentBankIndex() const override; + uint32_t getNumberOfBytesWritten() const override; + + uint32_t write(uint8_t *buffer, uint32_t bytesToWrite) override; + + void close() override; + +protected: +private: + FILE *outputFile_; + uint32_t bytesWritten_; +}; + +/** + * This class directs the copy process from the source to the specified output file + * + * It exists to abstract the source (rom/SRAM) and control the copy flow/speed and to + * allow us to give UI feedback on the copy process. + * + * After all: I found that the transfer pak is able to read 32 bytes every 1,5-2milliseconds + * We don't want the UI to remain frozen during that time. + * (source: http://n64devkit.square7.ch/pro-man/pro26/26-07.htm) + */ +class TransferPakDataCopier +{ +public: + TransferPakDataCopier(ITransferPakDataCopySource &source, ITransferPakDataCopyDestination &destination); + ~TransferPakDataCopier(); + + uint16_t getCurrentBankIndex() const; + uint32_t getNumberOfBytesRead() const; + size_t copyChunk(uint32_t numBytesToCopy); + +protected: +private: + ITransferPakDataCopySource &source_; + ITransferPakDataCopyDestination &destination_; +}; + +#endif \ No newline at end of file diff --git a/include/transferpak/TransferPakManager.h b/include/transferpak/TransferPakManager.h index 9e442f9..830f656 100755 --- a/include/transferpak/TransferPakManager.h +++ b/include/transferpak/TransferPakManager.h @@ -46,7 +46,7 @@ public: bool setPower(bool on); uint8_t getStatus(); - bool validateGbHeader(); + bool readCartridgeHeader(gameboy_cartridge_header& cartridgeHeader); /** * @brief This function switches the Gameboy ROM bank index diff --git a/include/widget/DialogWidget.h b/include/widget/DialogWidget.h index 8cfdf04..e665006 100755 --- a/include/widget/DialogWidget.h +++ b/include/widget/DialogWidget.h @@ -37,7 +37,11 @@ typedef struct DialogData // The next Dialog struct DialogData* next; + // this indicates whether DialogWidget should use delete to release the memory of this DialogData instance + // it should be set to false if it is statically allocated as a member of a class bool shouldDeleteWhenDone; + // This indicates that the user can't advance this dialog entry with A. + // it will be advanced from within the code instead. bool userAdvanceBlocked; //TODO: dialog sound } DialogData; diff --git a/include/widget/FileBrowserWidget.h b/include/widget/FileBrowserWidget.h new file mode 100644 index 0000000..964c497 --- /dev/null +++ b/include/widget/FileBrowserWidget.h @@ -0,0 +1,156 @@ +#ifndef SDCARDFILEBROWSERWIDGET_H +#define SDCARDFILEBROWSERWIDGET_H + +#include "core/Sprite.h" +#include "core/RDPQGraphics.h" +#include "widget/IWidget.h" +#include "widget/VerticalList.h" +#include "widget/MenuItemWidget.h" +#include "widget/ImageWidget.h" +#include "widget/IScrollWindowListener.h" + +#include + +typedef struct FileBrowserWidgetStyle +{ + VerticalListStyle listStyle; + MenuItemStyle itemStyle; + ImageWidgetStyle scrollArrowUpStyle; + ImageWidgetStyle scrollArrowDownStyle; +} FileBrowserWidgetStyle; + +typedef struct FileBrowserWidgetStatus +{ + std::vector& itemList; + int err; +} FileBrowserWidgetStatus; + +/** + * @brief This widget allows you to browse the SD card filesystem and + * select a file + * + */ +class FileBrowserWidget : public IWidget, public IScrollWindowListener +{ +public: + FileBrowserWidget(AnimationManager& animManager); + virtual ~FileBrowserWidget(); + + /** + * @brief Returns whether the widget is currently focused + */ + bool isFocused() const override; + + /** + * @brief Sets whether the widget is currently focused + * + */ + void setFocused(bool isFocused) override; + + /** + * @brief Returns whether the widget is currently visible + */ + bool isVisible() const override; + + /** + * @brief Changes the visibility of the widget + */ + void setVisible(bool visible) override; + + /** + * @brief Returns the current (relative) bounds of the widget + */ + Rectangle getBounds() const override; + + /** + * @brief Changes the current (relative) bounds of the widget + */ + void setBounds(const Rectangle& bounds) override; + + /** + * @brief Returns the size (width/height) of the widget + */ + Dimensions getSize() const override; + + /** + * @brief Sets the style of this widget + */ + void setStyle(const FileBrowserWidgetStyle& style); + + /** + * @brief Handles user input + * + * For button presses, it is advised to track button release situations instead of + * button presses for executing an action. Otherwise the key press might be handled again immediately + * in the next scene/widget because the user wouldn't have had the time to actually release the key. + */ + bool handleUserInput(const joypad_inputs_t& userInput) override; + + /** + * @brief Renders the widget + * + * @param gfx The graphics instance that must be used to render the widget + * @param parentBounds The bounds of the parent widget or scene. You must add the x,y offset of your own bounds + * to the parentBounds to get the absolute bounds for rendering. + * + * Getting the parentBounds as an argument of this function was done because a parent widget may be + * animated or change positions independent of the child widget. But when the parent widget moves, the child must as well! + */ + void render(RDPQGraphics& gfx, const Rectangle& parentBounds) override; + + const FileBrowserWidgetStatus& getStatus() const; + + /** + * @brief Retrieves the currently set path + */ + const char* getPath() const; + /** + * @brief Sets the current path of the FileBrowserWidget + */ + void setPath(const char* path); + + /** + * @brief Internal callback function for when you press A on a directory + */ + void onConfirmDirectory(const char* path); + + /** + * @brief Internal callback function for when you press A on a file + */ + void onConfirmFile(const char* path); + + void setItemConfirmedCallback(void (*onItemConfirmed)(void*, const char*), void* context); + + /** + * @brief Sets a file extension filter. The files that have such an extension will be shown, + * whereas non-matching files WON'T be shown + */ + void setFileExtensionToFilter(const char* fileExtensionFilter); + + void onScrollWindowChanged(const ScrollWindowUpdate& update) override; +protected: +private: + void clearList(); + void loadDirectoryItems(); + bool goToParentDirectory(); + + std::vector duplicatedDirEntNameList_; + // we are responsible for deleting the ItemMenuWidget instances + // so we need to keep track of them. + std::vector itemWidgetList_; + VerticalList listWidget_; + ImageWidget scrollArrowUp_; + ImageWidget scrollArrowDown_; + FileBrowserWidgetStyle style_; + FileBrowserWidgetStatus status_; + Rectangle bounds_; + char pathBuffer_[4096]; + void (*onItemConfirmedCallback_)(void*, const char*); + void* onItemConfirmedCallbackContext_; + const char* fileExtensionFilter_; + bool focused_; + bool visible_; + bool bButtonPressed_; +}; + +#endif \ No newline at end of file diff --git a/include/widget/ImageWidget.h b/include/widget/ImageWidget.h index 2b5b8de..33432dd 100644 --- a/include/widget/ImageWidget.h +++ b/include/widget/ImageWidget.h @@ -32,7 +32,7 @@ typedef struct ImageWidgetStyle SpriteRenderSettings spriteSettings; /** - * relative bounds of the icon sprite in relation to the MenuItem widget + * relative bounds of the icon sprite in relation to the ImageWidget widget */ Rectangle spriteBounds; } image; diff --git a/include/widget/MenuItemWidget.h b/include/widget/MenuItemWidget.h index f152ac1..53fbc70 100755 --- a/include/widget/MenuItemWidget.h +++ b/include/widget/MenuItemWidget.h @@ -95,6 +95,7 @@ public: MenuItemWidget(); virtual ~MenuItemWidget(); + const MenuItemData& getData() const; void setData(const MenuItemData& data); void setStyle(const MenuItemStyle& style); diff --git a/include/widget/ProgressBarWidget.h b/include/widget/ProgressBarWidget.h new file mode 100644 index 0000000..1945b48 --- /dev/null +++ b/include/widget/ProgressBarWidget.h @@ -0,0 +1,109 @@ +#ifndef _PROGRESSBARWIDGET_H +#define _PROGRESSBARWIDGET_H + +#include "widget/IWidget.h" +#include "core/Sprite.h" +#include "core/RDPQGraphics.h" + +typedef struct ProgressBarWidgetStyle +{ + struct{ + sprite_t* sprite; + SpriteRenderSettings renderSettings; + } background; + struct{ + struct{ + int left; + int right; + int top; + int bottom; + } margin; + color_t color; + sprite_t* sprite; + SpriteRenderSettings spriteSettings; + } bar; + TextRenderSettings textSettings; +} ProgressBarWidgetStyle; + +class ProgressBarWidget : public IWidget +{ +public: + ProgressBarWidget(); + virtual ~ProgressBarWidget(); + + /** + * Sets the current progress. Should be a value in the [0.0 - 1.0] interval + */ + void setProgress(double progress); + + /** + * Sets the ProgressBarWidget style + */ + void setStyle(const ProgressBarWidgetStyle& style); + + /** + * @brief Returns whether the widget is currently focused + */ + bool isFocused() const override; + + /** + * @brief Sets whether the widget is currently focused + * + */ + void setFocused(bool isFocused) override; + + /** + * @brief Returns whether the widget is currently visible + */ + bool isVisible() const override; + + /** + * @brief Changes the visibility of the widget + */ + void setVisible(bool visible) override; + + /** + * @brief Returns the current (relative) bounds of the widget + */ + Rectangle getBounds() const override; + + /** + * @brief Changes the current (relative) bounds of the widget + */ + void setBounds(const Rectangle& bounds) override; + + /** + * @brief Returns the size (width/height) of the widget + */ + Dimensions getSize() const override; + + /** + * @brief Handles user input + * + * For button presses, it is advised to track button release situations instead of + * button presses for executing an action. Otherwise the key press might be handled again immediately + * in the next scene/widget because the user wouldn't have had the time to actually release the key. + */ + bool handleUserInput(const joypad_inputs_t& userInput) override; + + /** + * @brief Renders the widget + * + * @param gfx The graphics instance that must be used to render the widget + * @param parentBounds The bounds of the parent widget or scene. You must add the x,y offset of your own bounds + * to the parentBounds to get the absolute bounds for rendering. + * + * Getting the parentBounds as an argument of this function was done because a parent widget may be + * animated or change positions independent of the child widget. But when the parent widget moves, the child must as well! + */ + void render(RDPQGraphics& gfx, const Rectangle& parentBounds) override; +protected: +private: + ProgressBarWidgetStyle style_; + bool visible_; + Rectangle bounds_; + double progress_; + char textBuffer_[5]; +}; + +#endif \ No newline at end of file diff --git a/include/widget/TransferPakDetectionWidget.h b/include/widget/TransferPakDetectionWidget.h index 6c514e3..07f0c27 100755 --- a/include/widget/TransferPakDetectionWidget.h +++ b/include/widget/TransferPakDetectionWidget.h @@ -16,10 +16,13 @@ enum class TransferPakWidgetState DETECTING_PAK, VALIDATING_GB_HEADER, DETECTING_GAME, + VALIDATING_GAME_SAVE, GB_HEADER_VALIDATION_FAILED, NO_TRANSFER_PAK_FOUND, NO_GAME_FOUND, - GAME_FOUND + GAME_FOUND, + VALID_SAVE_FOUND, + NO_SAVE_FOUND }; typedef struct TransferPakDetectionWidgetStyle @@ -108,6 +111,7 @@ private: void switchState(TransferPakWidgetState previousState, TransferPakWidgetState newState); void renderUnknownState(RDPQGraphics& gfx, const Rectangle& parentBounds); + void renderValidatingSaveState(RDPQGraphics& gfx, const Rectangle& parentBounds); void renderErrorState(RDPQGraphics& gfx, const Rectangle& parentBounds); /** @@ -132,6 +136,8 @@ private: void updateCartridgeIcon(); + bool validateGameSave(); + TransferPakDetectionWidgetStyle style_; AnimationManager& animManager_; TransferPakManager& tpakManager_; diff --git a/libpokemegb b/libpokemegb index 33b4f9c..de5381d 160000 --- a/libpokemegb +++ b/libpokemegb @@ -1 +1 @@ -Subproject commit 33b4f9ce2f2679271ff089f1faf7400dfb7ee1c9 +Subproject commit de5381da81ac41741c75452e98caedcf28c5c3b0 diff --git a/src/core/Application.cpp b/src/core/Application.cpp index 1b12afa..641a337 100755 --- a/src/core/Application.cpp +++ b/src/core/Application.cpp @@ -1,5 +1,6 @@ #include "core/Application.h" #include "scenes/IScene.h" +#include "core/DragonUtils.h" static Application* appInstance = nullptr; static void resetInterruptHandler() @@ -40,6 +41,7 @@ void Application::init() { // Based on example code https://github.com/DragonMinded/libdragon/wiki/OpenGL-on-N64 debug_init_isviewer(); + mountSDCard(); //console_set_debug(true); joypad_init(); diff --git a/src/core/DragonUtils.cpp b/src/core/DragonUtils.cpp index c5840f0..3480b4d 100755 --- a/src/core/DragonUtils.cpp +++ b/src/core/DragonUtils.cpp @@ -1,4 +1,7 @@ #include "core/DragonUtils.h" +#include "libcart/cart.h" + +bool sdcard_mounted = false; static uint8_t ANALOG_STICK_THRESHOLD = 30; @@ -45,4 +48,87 @@ const UINavigationDirection determineUINavigationDirection(joypad_inputs_t input } } return UINavigationDirection::MAX; +} + +bool mountSDCard() +{ + sdcard_mounted = debug_init_sdfs("sd:/", -1); + return sdcard_mounted; +} + +size_t writeBufferToFile(const char* path, const uint8_t* buffer, size_t bufferSize) +{ + size_t ret; + if(!sdcard_mounted) + { + return 0; + } + + FILE* f = fopen(path, "w"); + if(!f) + { + return 0; + } + + ret = fwrite(buffer, sizeof(char), bufferSize, f); + + fclose(f); + return ret; +} + +uint32_t convertROMSizeIntoNumBytes(gb_cart_rom_size_t romSize) +{ + switch(romSize) + { + case GB_ROM_32KB: + return 32 * 1024; + case GB_ROM_64KB: + return 64 * 1024; + case GB_ROM_128KB: + return 128 * 1024; + case GB_ROM_256KB: + return 256 * 1024; + case GB_ROM_512KB: + return 512 * 1024; + case GB_ROM_1MB: + return 1024 * 1024; + case GB_ROM_2MB: + return 2048 * 1024; + case GB_ROM_4MB: + return 4096 * 1024; + case GB_ROM_8MB: + return 8192 * 1024; + case GB_ROM_1152KB: + return 1152 * 1024; + case GB_ROM_1280KB: + return 1280 * 1024; + case GB_ROM_1536KB: + return 1536 * 1024; + default: + return 0; + } +} + +uint32_t convertSRAMSizeIntoNumBytes(gb_cart_ram_size_t ramSize) +{ + switch(ramSize) + { + case GB_RAM_2KB: + return 2 * 1024; + case GB_RAM_8KB: + return 8 * 1024; + case GB_RAM_32KB: + return 32 * 1024; + case GB_RAM_64KB: + return 64 * 1024; + case GB_RAM_128KB: + return 128 * 1024; + default: + return 0; + } +} + +bool doesN64FlashCartSupportSDCardAccess() +{ + return (cart_type > CART_NULL && cart_type < CART_MAX); } \ No newline at end of file diff --git a/src/core/common.cpp b/src/core/common.cpp index 41d1321..fd4c60c 100755 --- a/src/core/common.cpp +++ b/src/core/common.cpp @@ -1,5 +1,32 @@ #include "core/common.h" +#include + +ManagedString::ManagedString(char* rawString) + : rawString_(rawString) +{ +} + +ManagedString::~ManagedString() +{ + free(rawString_); + rawString_ = nullptr; +} + +const char* ManagedString::get() const +{ + return rawString_; +} + +void ManagedString::operator = (char* rawString) +{ + if(rawString_) + { + free(rawString_); + } + rawString_ = rawString; +} + bool isZeroSizeRectangle(const Rectangle &rect) { return (!rect.width || !rect.height); diff --git a/src/menu/MenuEntries.cpp b/src/menu/MenuEntries.cpp index f25c45a..778b406 100755 --- a/src/menu/MenuEntries.cpp +++ b/src/menu/MenuEntries.cpp @@ -2,6 +2,10 @@ #include "menu/MenuFunctions.h" MenuItemData gen1MenuEntries[] = { + { + .title = "Backup/Restore", + .onConfirmAction = goToBackupRestoreMenu + }, { .title = "Event Pokémon", .onConfirmAction = goToGen1DistributionPokemonMenu @@ -29,6 +33,10 @@ MenuItemData gen1MenuEntries[] = { const uint32_t gen1MenuEntriesSize = sizeof(gen1MenuEntries); MenuItemData gen2MenuEntries[] = { + { + .title = "Backup/Restore", + .onConfirmAction = goToBackupRestoreMenu + }, { .title = "Event Pokémon", .onConfirmAction = goToGen2DistributionPokemonMenu @@ -45,6 +53,10 @@ MenuItemData gen2MenuEntries[] = { .title = "Gen 3 Transfer Info", .onConfirmAction = goToPokeTransporterGBRef }, + { + .title = "Reset Game Clock", + .onConfirmAction = resetRTC + }, { .title = "About", .onConfirmAction = goToAboutScene @@ -54,6 +66,10 @@ MenuItemData gen2MenuEntries[] = { const uint32_t gen2MenuEntriesSize = sizeof(gen2MenuEntries); MenuItemData gen2CrystalMenuEntries[] = { + { + .title = "Backup/Restore", + .onConfirmAction = goToBackupRestoreMenu + }, { .title = "Event Pokémon", .onConfirmAction = goToGen2DistributionPokemonMenu @@ -74,6 +90,10 @@ MenuItemData gen2CrystalMenuEntries[] = { .title = "Gen 3 Transfer Info", .onConfirmAction = goToPokeTransporterGBRef }, + { + .title = "Reset Game Clock", + .onConfirmAction = resetRTC + }, { .title = "About", .onConfirmAction = goToAboutScene @@ -101,3 +121,27 @@ MenuItemData gen2DecorationMenuEntries[] = { }; const uint32_t gen2DecorationMenuEntriesSize = sizeof(gen2DecorationMenuEntries); + +MenuItemData backupRestoreMenuEntries[] = { + { + .title = "Backup Save", + .onConfirmAction = goToDataCopyScene, + .itemParam = &DATACOPY_BACKUP_SAVE + }, + { + .title = "Backup ROM", + .onConfirmAction = goToDataCopyScene, + .itemParam = &DATACOPY_BACKUP_ROM + }, + { + .title = "Restore Save", + .onConfirmAction = goToDataCopyScene, + .itemParam = &DATACOPY_RESTORE_SAVE + }, + { + .title = "Wipe Save", + .onConfirmAction = askConfirmationWipeSave + } +}; + +const uint32_t backupRestoreMenuEntriesSize = sizeof(backupRestoreMenuEntries); \ No newline at end of file diff --git a/src/menu/MenuFunctions.cpp b/src/menu/MenuFunctions.cpp index c5b4b40..7fe31b9 100755 --- a/src/menu/MenuFunctions.cpp +++ b/src/menu/MenuFunctions.cpp @@ -4,6 +4,7 @@ #include "scenes/DistributionPokemonListScene.h" #include "scenes/StatsScene.h" #include "scenes/MenuScene.h" +#include "scenes/SelectFileScene.h" #include "scenes/SceneManager.h" #include "gen2/Gen2GameReader.h" #include "transferpak/TransferPakManager.h" @@ -15,6 +16,11 @@ const Move MOVE_SURF = Move::SURF; const Move MOVE_FLY = Move::FLY; +const DataCopyOperation DATACOPY_BACKUP_SAVE = DataCopyOperation::BACKUP_SAVE; +const DataCopyOperation DATACOPY_BACKUP_ROM = DataCopyOperation::BACKUP_ROM; +const DataCopyOperation DATACOPY_RESTORE_SAVE = DataCopyOperation::RESTORE_SAVE; +const DataCopyOperation DATACOPY_WIPE_SAVE = DataCopyOperation::WIPE_SAVE; + // based on https://github.com/kwsch/PKHeX/blob/master/PKHeX.Core/Resources/text/script/gen2/flags_c_en.txt const uint16_t GEN2_EVENTFLAG_DECORATION_PIKACHU_BED = 679; const uint16_t GEN2_EVENTFLAG_DECORATION_UNOWN_DOLL = 712; @@ -132,6 +138,36 @@ void goToAboutScene(void* context, const void* param) sceneManager.switchScene(SceneType::ABOUT); } +void goToDataCopyScene(void* context, const void* param) +{ + MenuScene* scene = static_cast(context); + const DataCopyOperation operation = (*((const DataCopyOperation*)param)); + SceneManager& sceneManager = scene->getDependencies().sceneManager; + + auto dataCopyContext = new DataCopySceneContext{ + .operation = operation, + .saveToRestorePath = nullptr + }; + + if(operation == DataCopyOperation::RESTORE_SAVE) + { + auto fileSelectContext = new SelectFileSceneContext{ + .titleText = "Select Save file", + .nextScene = { + .type = SceneType::COPY_DATA, + .context = dataCopyContext, + .deleteContextFunc = deleteDataCopySceneContext + }, + .fileExtensionFilter = ".sav" + }; + sceneManager.switchScene(SceneType::SELECT_FILE, deleteSelectFileSceneContext, fileSelectContext); + } + else + { + sceneManager.switchScene(SceneType::COPY_DATA, deleteDataCopySceneContext, dataCopyContext); + } +} + void goToGen1DistributionPokemonMenu(void* context, const void*) { goToDistributionPokemonListMenu(context, DistributionPokemonListType::GEN1); @@ -158,6 +194,17 @@ void goToGen2DecorationMenu(void* context, const void* param) scene->getDependencies().sceneManager.switchScene(SceneType::MENU, deleteMenuSceneContext, newSceneContext); } +void goToBackupRestoreMenu(void* context, const void* param) +{ + MenuScene* scene = static_cast(context); + auto newSceneContext = new MenuSceneContext{ + .menuEntries = backupRestoreMenuEntries, + .numMenuEntries = backupRestoreMenuEntriesSize / sizeof(backupRestoreMenuEntries[0]) + }; + + scene->getDependencies().sceneManager.switchScene(SceneType::MENU, deleteMenuSceneContext, newSceneContext); +} + void gen1PrepareToTeachPikachu(void* context, const void* param) { MenuScene* scene = static_cast(context); @@ -369,8 +416,6 @@ void gen2ReceiveGSBall(void* context, const void* param) }; tpakManager.setRAMEnabled(true); - - const char* trainerName = gameReader.getTrainerName(); // the unlockGsBallEvent() function does all the work. It's even repeatable! gameReader.unlockGsBallEvent(); @@ -378,7 +423,7 @@ void gen2ReceiveGSBall(void* context, const void* param) tpakManager.finishWrites(); tpakManager.setRAMEnabled(false); - setDialogDataText(*messageData, "GS Ball event unlocked! Please go to the Golden Rod Pokémon Center and try to leave!", trainerName); + setDialogDataText(*messageData, "GS Ball event unlocked! Please go to the Golden Rod Pokémon Center and try to leave!"); scene->showDialog(messageData); } @@ -397,7 +442,7 @@ void gen2SetEventFlag(void* context, const void* param) tpakManager.setRAMEnabled(true); - const char* trainerName = gameReader.getTrainerName(); + const char* trainerName = scene->getDependencies().playerName; if(gameReader.getEventFlag(eventFlagIndex)) { setDialogDataText(*messageData, "%s already has %s!", trainerName, convertGen2EventFlagToString(eventFlagIndex)); @@ -414,3 +459,68 @@ void gen2SetEventFlag(void* context, const void* param) tpakManager.setRAMEnabled(false); scene->showDialog(messageData); } + +void askConfirmationWipeSave(void* context, const void* param) +{ + MenuScene* scene = static_cast(context); + + DialogData* messageData = new DialogData{ + .options = { + .items = new MenuItemData[2]{ + { + .title = "Yes", + .onConfirmAction = goToDataCopyScene, + .context = context, + .itemParam = &DATACOPY_WIPE_SAVE + }, + { + .title = "No", + .onConfirmAction = advanceDialog, + .context = context + } + }, + .number = 2, + .shouldDeleteWhenDone = true + }, + .shouldDeleteWhenDone = true, + }; + + setDialogDataText(*messageData, "Are you sure you want to wipe the save file from the cartridge?"); + + scene->showDialog(messageData); +} + +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{ + .shouldDeleteWhenDone = true + }; + + if(scene->getDependencies().generation != 2) + { + setDialogDataText(*diag, "Sorry! This is only supported for Gen 2 Pokémon games!"); + scene->showDialog(diag); + return; + } + + TransferPakManager& tpakManager = scene->getDependencies().tpakManager; + + tpakManager.setRAMEnabled(true); + + tpakManager.switchGBSRAMBank(0); + + tpakManager.writeSRAM(0xC60, &rtcStatusFieldValue, 1); + tpakManager.finishWrites(); + + setDialogDataText(*diag, "The games' clock was reset! Start the game to reconfigure it! Don't forget to save!"); + scene->showDialog(diag); +} \ No newline at end of file diff --git a/src/scenes/AboutScene.cpp b/src/scenes/AboutScene.cpp index 8fc801b..29f5744 100644 --- a/src/scenes/AboutScene.cpp +++ b/src/scenes/AboutScene.cpp @@ -31,7 +31,7 @@ github.com/LinusU/pokemon-sprite-compression github.com/xvillaneau/poke-sprite-python )delim"; -static const char* headerTextString = R"delim(PokeMe64 Version 0.1 +static const char* headerTextString = R"delim(PokeMe64 Version 0.2 by risingPhil SPECIAL THANKS TO: diff --git a/src/scenes/AbstractUIScene.cpp b/src/scenes/AbstractUIScene.cpp index c457ab1..20145c2 100755 --- a/src/scenes/AbstractUIScene.cpp +++ b/src/scenes/AbstractUIScene.cpp @@ -94,4 +94,9 @@ void AbstractUIScene::setFocusChain(WidgetFocusChainSegment* focusChain) { focusChain_->current->setFocused(true); } +} + +SceneDependencies& AbstractUIScene::getDependencies() +{ + return deps_; } \ No newline at end of file diff --git a/src/scenes/DataCopyScene.cpp b/src/scenes/DataCopyScene.cpp new file mode 100644 index 0000000..59a12c7 --- /dev/null +++ b/src/scenes/DataCopyScene.cpp @@ -0,0 +1,359 @@ +#include "scenes/DataCopyScene.h" +#include "core/DragonUtils.h" +#include "scenes/SceneManager.h" +#include "menu/MenuFunctions.h" +#include "gen1/Gen1Common.h" +#include "gen2/Gen2Common.h" + +#include + +//missing function declaration in libdragons' system.h, but the definition exists in system.c +int mkdir( const char * path, mode_t mode ); + +/** + * Copying from or to the transfer pak is a blocking operation. + * So while we're doing that, we can't render anything. + * + * So, in order to not just entirely freeze until the copy operation is done, we copy + * in chunks. + * + * We know from http://n64devkit.square7.ch/pro-man/pro26/26-07.htm + * that the transfer pak is able to read 32 bytes every 1,5 - 2 milliseconds. + * + * Our copy operation at the moment isn't entirely efficient, so it likely is slower. + * But just basing off that number, 4096 bytes should be transferred within +- 256 ms + * + * That means we would theoretically render at 4fps during the copy with this chunk size. + * (due to our copy implementation, it's likely less though) + */ +static int COPY_CHUNK_SIZE_IN_BYTES = 4096; + +static void dialogFinishedCallback(void* context) +{ + DataCopyScene* scene = (DataCopyScene*)context; + scene->onDialogDone(); +} + +/** + * @brief The reason this function exists is because I first tried to use the cartridge header title + trainerName (max 11 chars) + unique number as the game save filename + * but it turned out being a bit too close to fill the entire second line of the DialogWidget because the path became too long. + * The solution is to just create a shorter game title (just "Blue" or "Red" or "Crystal"). That frees up some room in the DialogWidget + * for the trainername and save number + */ +static void generateRomTitle(char* outputPath, const gameboy_cartridge_header& gbHeader, uint8_t generation, uint8_t specificGenVersion) +{ + if(generation == 1) + { + switch(static_cast(specificGenVersion)) + { + case Gen1GameType::BLUE: + strcpy(outputPath, "Blue"); + break; + case Gen1GameType::RED: + strcpy(outputPath, "Red"); + break; + case Gen1GameType::YELLOW: + strcpy(outputPath, "Yellow"); + break; + default: + strcpy(outputPath, "Unknown"); + break; + } + } + else if(generation == 2) + { + switch(static_cast(specificGenVersion)) + { + case Gen2GameType::GOLD: + strcpy(outputPath, "Gold"); + break; + case Gen2GameType::SILVER: + strcpy(outputPath, "Silver"); + break; + case Gen2GameType::CRYSTAL: + strcpy(outputPath, "Crystal"); + break; + default: + strcpy(outputPath, "Unknown"); + break; + } + } + else + { + // the title field of the gameboy header is likely truncated. + // create a copy and make sure to append a null character so we won't crash when trying to use it as a string + memcpy(outputPath, gbHeader.new_title.title, 11); + outputPath[11] = '\0'; + } +} + +static void generateSaveFileName(char* savOutputPath, size_t bufferSize, const char* gameTitle, const char* playerName) +{ + struct stat statStruct; + unsigned uniqueNumber = 0; + const size_t playerNameSize = strlen(playerName); + + if(playerNameSize) + { + snprintf(savOutputPath, bufferSize - 1, "sd:/PokeMe64/%s_%s.sav", gameTitle, playerName); + } + else + { + snprintf(savOutputPath, bufferSize - 1, "sd:/PokeMe64/%s.sav", gameTitle); + } + + while(stat(savOutputPath, &statStruct) == 0) + { + if(playerNameSize) + { + snprintf(savOutputPath, bufferSize - 1, "sd:/PokeMe64/%s_%s_%u.sav", gameTitle, playerName, uniqueNumber); + } + else + { + snprintf(savOutputPath, bufferSize - 1, "sd:/PokeMe64/%s_%u.sav", gameTitle, uniqueNumber); + } + ++uniqueNumber; + } +} + +DataCopyScene::DataCopyScene(SceneDependencies& deps, void* context) + : SceneWithProgressBar(deps) + , romReader_(deps.tpakManager) + , saveManager_(deps.tpakManager) + , sceneContext_((DataCopySceneContext*)context) + , copySource_(nullptr) + , copyDestination_(nullptr) + , copier_(nullptr) + , dialogWidgetSprite_(nullptr) + , progressBackgroundSprite_(nullptr) + , diag_({0}) + , totalBytesToCopy_(0) +{ + (void)context; +} + +DataCopyScene::~DataCopyScene() +{ +} + +void DataCopyScene::init() +{ + char savOutputPath[4096]; + char romOutputPath[4096]; + char gameTitle[12]; + dialogWidgetSprite_ = sprite_load("rom://menu-bg-9slice.sprite"); + progressBackgroundSprite_ = sprite_load("rom://bg-nineslice-transparant-border.sprite"); + + SceneWithProgressBar::init(); + + // check if the n64 flashcart is supported + if(!doesN64FlashCartSupportSDCardAccess()) + { + setDialogDataText(diag_, "Sorry! This is only supported on 64Drive, Everdrive64, ED64Plus and SummerCart64!"); + showDialog(&diag_); + return; + } + + // check if the sd card is mounted + if(!sdcard_mounted) + { + setDialogDataText(diag_, "ERROR: SD card is not mounted!"); + showDialog(&diag_); + return; + } + + mkdir("sd:/PokeMe64", 0777); + + gameboy_cartridge_header gbHeader; + deps_.tpakManager.readCartridgeHeader(gbHeader); + + generateRomTitle(gameTitle, gbHeader, deps_.generation, deps_.specificGenVersion); + + auto msg2 = new DialogData{ + .shouldDeleteWhenDone = true + }; + + switch(sceneContext_->operation) + { + case DataCopyOperation::BACKUP_SAVE: + generateSaveFileName(savOutputPath, sizeof(savOutputPath), gameTitle, deps_.playerName); + copySource_ = new TransferPakSaveManagerCopySource(saveManager_); + copyDestination_ = new TransferPakFileCopyDestination(savOutputPath); + totalBytesToCopy_ = convertSRAMSizeIntoNumBytes(gbHeader.ram_size_code); + setDialogDataText(*msg2, "The save was backed up to %s!", savOutputPath); + break; + case DataCopyOperation::BACKUP_ROM: + snprintf(romOutputPath, sizeof(savOutputPath) - 1, "sd:/PokeMe64/%s.gbc", gameTitle); + copySource_ = new TransferPakRomReaderCopySource(romReader_); + copyDestination_ = new TransferPakFileCopyDestination(romOutputPath); + totalBytesToCopy_ = convertROMSizeIntoNumBytes(gbHeader.rom_size_code); + setDialogDataText(*msg2, "The cartridge rom was backed up to %s!", romOutputPath); + break; + case DataCopyOperation::RESTORE_SAVE: + copySource_ = new TransferPakFileCopySource(sceneContext_->saveToRestorePath.get()); + copyDestination_ = new TransferPakSaveManagerDestination(saveManager_); + totalBytesToCopy_ = convertSRAMSizeIntoNumBytes(gbHeader.ram_size_code); + setDialogDataText(*msg2, "The save was restored to the cartridge!", romOutputPath); + break; + case DataCopyOperation::WIPE_SAVE: + copySource_ = new TransferPakNullCopySource(); + copyDestination_ = new TransferPakSaveManagerDestination(saveManager_); + totalBytesToCopy_ = convertSRAMSizeIntoNumBytes(gbHeader.ram_size_code); + setDialogDataText(*msg2, "The save file was wiped from the cartridge!"); + break; + } + + if(!copySource_->readyForTransfer()) + { + if(sceneContext_->operation == DataCopyOperation::RESTORE_SAVE) + { + setDialogDataText(diag_, "ERROR: Could not read from file %s!", sceneContext_->saveToRestorePath.get()); + } + else + { + setDialogDataText(diag_, "ERROR: Could not read from cartridge!"); + } + + // not needed + delete msg2; + msg2 = nullptr; + + // now show the error dialog + showDialog(&diag_); + return; + } + + if(!copyDestination_->readyForTransfer()) + { + if(sceneContext_->operation == DataCopyOperation::RESTORE_SAVE) + { + setDialogDataText(diag_, "ERROR: Could not write to cartridge!"); + } + else + { + const char* outputPath = (sceneContext_->operation == DataCopyOperation::BACKUP_SAVE) ? savOutputPath : romOutputPath; + setDialogDataText(diag_, "ERROR: Could not write to file %s!", outputPath); + } + + // not needed + delete msg2; + msg2 = nullptr; + // now show the error dialog + showDialog(&diag_); + return; + } + + if(sceneContext_->operation == DataCopyOperation::WIPE_SAVE) + { + setDialogDataText(diag_, "Wiping. Please Wait..."); + } + { + setDialogDataText(diag_, "Copying. Please Wait..."); + } + diag_.userAdvanceBlocked = true; + diag_.next = msg2; + showDialog(&diag_); + + deps_.tpakManager.setRAMEnabled(true); + copier_ = new TransferPakDataCopier(*copySource_, *copyDestination_); +} + +void DataCopyScene::destroy() +{ + sprite_free(dialogWidgetSprite_); + dialogWidgetSprite_ = nullptr; + sprite_free(progressBackgroundSprite_); + progressBackgroundSprite_ = nullptr; + + if(copier_) + { + delete copier_; + copier_ = nullptr; + } + + if(copySource_) + { + delete copySource_; + copySource_ = nullptr; + } + + if(copyDestination_) + { + delete copyDestination_; + copyDestination_ = nullptr; + } + + deps_.tpakManager.setRAMEnabled(false); + SceneWithProgressBar::destroy(); +} + +void DataCopyScene::processUserInput() +{ + if(copier_ && copyDestination_ && copyDestination_->getNumberOfBytesWritten() < totalBytesToCopy_) + { + const uint32_t numBytesToCopy = std::min(COPY_CHUNK_SIZE_IN_BYTES, totalBytesToCopy_ - copyDestination_->getNumberOfBytesWritten()); + copier_->copyChunk(numBytesToCopy); + + setProgress(static_cast(copyDestination_->getNumberOfBytesWritten()) / static_cast(totalBytesToCopy_)); + } + + if(copier_ && copyDestination_ && copyDestination_->getNumberOfBytesWritten() >= totalBytesToCopy_) + { + deps_.tpakManager.setRAMEnabled(false); + copyDestination_->close(); + delete copySource_; + copySource_ = nullptr; + delete copyDestination_; + copyDestination_ = nullptr; + delete copier_; + + // The copy operation is done, now advance the blocked dialog entry to the final one + advanceDialog(); + } + + SceneWithProgressBar::processUserInput(); +} + +void DataCopyScene::render(RDPQGraphics& gfx, const Rectangle& sceneBounds) +{ + SceneWithProgressBar::render(gfx, sceneBounds); +} + +void DataCopyScene::onDialogDone() +{ + deps_.sceneManager.goBackToPreviousScene(); +} + +void DataCopyScene::setupDialog(DialogWidgetStyle& style) +{ + style.background.sprite = dialogWidgetSprite_; + style.background.spriteSettings = { + .renderMode = SpriteRenderMode::NINESLICE, + .srcRect = { 6, 6, 6, 6 } + }; + + SceneWithProgressBar::setupDialog(style); + + dialogWidget_.setOnDialogFinishedCallback(dialogFinishedCallback, this); + dialogWidget_.setVisible(false); +} + +void DataCopyScene::setupProgressBar(ProgressBarWidgetStyle& style) +{ + SceneWithProgressBar::setupProgressBar(style); + + style.background = { + .sprite = progressBackgroundSprite_, + .renderSettings = { + .renderMode = SpriteRenderMode::NINESLICE, + .srcRect = { 6, 6, 6, 6 } + } + }; +} + +void deleteDataCopySceneContext(void* context) +{ + DataCopySceneContext* sceneContext = (DataCopySceneContext*)context; + + delete sceneContext; +} \ No newline at end of file diff --git a/src/scenes/InitTransferPakScene.cpp b/src/scenes/InitTransferPakScene.cpp index 357f0f6..f9e656c 100755 --- a/src/scenes/InitTransferPakScene.cpp +++ b/src/scenes/InitTransferPakScene.cpp @@ -31,11 +31,8 @@ InitTransferPakScene::InitTransferPakScene(SceneDependencies& deps, void*) }) , diagData_({0}) , pokeMe64TextSettings_() - , playerName_() , gameTypeString_(nullptr) { - playerName_[0] = '\0'; - playerName_[PLAYER_NAME_SIZE - 1] = '\0'; } InitTransferPakScene::~InitTransferPakScene() @@ -68,7 +65,7 @@ void InitTransferPakScene::destroy() void InitTransferPakScene::render(RDPQGraphics& gfx, const Rectangle& sceneBounds) { - gfx.drawText(Rectangle{0, 10, 320, 16}, "PokeMe64 by risingPhil. Version 0.1", pokeMe64TextSettings_); + gfx.drawText(Rectangle{0, 10, 320, 16}, "PokeMe64 by risingPhil. Version 0.2", pokeMe64TextSettings_); tpakDetectWidget_.render(gfx, sceneBounds); SceneWithDialogWidget::render(gfx, sceneBounds); @@ -76,37 +73,49 @@ void InitTransferPakScene::render(RDPQGraphics& gfx, const Rectangle& sceneBound void InitTransferPakScene::onDialogDone() { - MenuSceneContext* menuContext; - Gen1GameType gen1Type; - Gen2GameType gen2Type; + MenuSceneContext* menuContext = nullptr; - tpakDetectWidget_.retrieveGameType(gen1Type, gen2Type); - - if(gen1Type != Gen1GameType::INVALID) + if(tpakDetectWidget_.getState() == TransferPakWidgetState::NO_SAVE_FOUND) { - menuContext = new MenuSceneContext({ - .menuEntries = gen1MenuEntries, - .numMenuEntries = static_cast(gen1MenuEntriesSize / sizeof(gen1MenuEntries[0])), + menuContext = new MenuSceneContext{ + .menuEntries = backupRestoreMenuEntries, + .numMenuEntries = static_cast(backupRestoreMenuEntriesSize / sizeof(backupRestoreMenuEntries[0])), .bButtonMeansUserWantsToSwitchCartridge = true - }); + }; } - else if(gen2Type != Gen2GameType::INVALID) + else if(tpakDetectWidget_.getState() == TransferPakWidgetState::VALID_SAVE_FOUND) { - if(gen2Type == Gen2GameType::CRYSTAL) + Gen1GameType gen1Type; + Gen2GameType gen2Type; + + tpakDetectWidget_.retrieveGameType(gen1Type, gen2Type); + + if(gen1Type != Gen1GameType::INVALID) { menuContext = new MenuSceneContext({ - .menuEntries = gen2CrystalMenuEntries, - .numMenuEntries = static_cast(gen2CrystalMenuEntriesSize / sizeof(gen2CrystalMenuEntries[0])), + .menuEntries = gen1MenuEntries, + .numMenuEntries = static_cast(gen1MenuEntriesSize / sizeof(gen1MenuEntries[0])), .bButtonMeansUserWantsToSwitchCartridge = true }); } - else + else if(gen2Type != Gen2GameType::INVALID) { - menuContext = new MenuSceneContext({ - .menuEntries = gen2MenuEntries, - .numMenuEntries = static_cast(gen2MenuEntriesSize / sizeof(gen2MenuEntries[0])), - .bButtonMeansUserWantsToSwitchCartridge = true - }); + if(gen2Type == Gen2GameType::CRYSTAL) + { + menuContext = new MenuSceneContext({ + .menuEntries = gen2CrystalMenuEntries, + .numMenuEntries = static_cast(gen2CrystalMenuEntriesSize / sizeof(gen2CrystalMenuEntries[0])), + .bButtonMeansUserWantsToSwitchCartridge = true + }); + } + else + { + menuContext = new MenuSceneContext({ + .menuEntries = gen2MenuEntries, + .numMenuEntries = static_cast(gen2MenuEntriesSize / sizeof(gen2MenuEntries[0])), + .bButtonMeansUserWantsToSwitchCartridge = true + }); + } } } else @@ -121,11 +130,12 @@ void InitTransferPakScene::onDialogDone() void InitTransferPakScene::onTransferPakWidgetStateChanged(TransferPakWidgetState newState) { debugf("onTransferPakWidgetStateChanged(%d)\r\n", static_cast(newState)); - if(newState == TransferPakWidgetState::GAME_FOUND) + if(newState == TransferPakWidgetState::VALID_SAVE_FOUND) { debugf("[InitTransferPakScene]: Game found!\r\n"); + loadGameType(); deps_.tpakManager.setRAMEnabled(true); - loadGameMetadata(); + loadSaveMetadata(); /* Quote: * "It is recommended to disable external RAM after accessing it, in order to protect its contents from corruption * during power down of the Game Boy or removal of the cartridge. Once the cartridge has completely lost power from @@ -138,7 +148,17 @@ void InitTransferPakScene::onTransferPakWidgetStateChanged(TransferPakWidgetStat */ deps_.tpakManager.setRAMEnabled(false); - setDialogDataText(diagData_, "Hi %s! We've detected Pokémon %s in the N64 Transfer Pak. Let's go!", playerName_, gameTypeString_); + setDialogDataText(diagData_, "Hi %s! We've detected Pokémon %s in the N64 Transfer Pak. Let's go!", deps_.playerName, gameTypeString_); + dialogWidget_.appendDialogData(&diagData_); + dialogWidget_.setVisible(true); + setFocusChain(&dialogFocusChainSegment_); + } + else if(newState == TransferPakWidgetState::NO_SAVE_FOUND) + { + debugf("[InitTransferPakScene]: Game found, but no save found!\r\n"); + loadGameType(); + + setDialogDataText(diagData_, "We can't find a save in your Pokemon %s cartridge. You'll only be able to backup/restore!", gameTypeString_); dialogWidget_.appendDialogData(&diagData_); dialogWidget_.setVisible(true); setFocusChain(&dialogFocusChainSegment_); @@ -190,19 +210,14 @@ void InitTransferPakScene::setupDialog(DialogWidgetStyle& style) dialogWidget_.setVisible(false); } -void InitTransferPakScene::loadGameMetadata() +void InitTransferPakScene::loadGameType() { - TransferPakRomReader romReader(deps_.tpakManager); - TransferPakSaveManager saveManager(deps_.tpakManager); Gen1GameType gen1Type; Gen2GameType gen2Type; tpakDetectWidget_.retrieveGameType(gen1Type, gen2Type); if(gen1Type != Gen1GameType::INVALID) { - Gen1GameReader gameReader(romReader, saveManager, gen1Type); - const char* trainerName = gameReader.getTrainerName(); - strncpy(playerName_, trainerName, PLAYER_NAME_SIZE - 1); deps_.generation = 1; deps_.specificGenVersion = static_cast(gen1Type); @@ -224,9 +239,6 @@ void InitTransferPakScene::loadGameMetadata() } else if(gen2Type != Gen2GameType::INVALID) { - Gen2GameReader gameReader(romReader, saveManager, gen2Type); - const char* trainerName = gameReader.getTrainerName(); - strncpy(playerName_, trainerName, PLAYER_NAME_SIZE - 1); deps_.generation = 2; deps_.specificGenVersion = static_cast(gen2Type); @@ -246,5 +258,27 @@ void InitTransferPakScene::loadGameMetadata() break; } } - playerName_[PLAYER_NAME_SIZE - 1] = '\0'; +} + +void InitTransferPakScene::loadSaveMetadata() +{ + TransferPakRomReader romReader(deps_.tpakManager); + TransferPakSaveManager saveManager(deps_.tpakManager); + Gen1GameType gen1Type; + Gen2GameType gen2Type; + + tpakDetectWidget_.retrieveGameType(gen1Type, gen2Type); + + if(gen1Type != Gen1GameType::INVALID) + { + Gen1GameReader gameReader(romReader, saveManager, gen1Type); + const char* trainerName = gameReader.getTrainerName(); + strncpy(deps_.playerName, trainerName, sizeof(deps_.playerName) - 1); + } + else if(gen2Type != Gen2GameType::INVALID) + { + Gen2GameReader gameReader(romReader, saveManager, gen2Type); + const char* trainerName = gameReader.getTrainerName(); + strncpy(deps_.playerName, trainerName, sizeof(deps_.playerName) - 1); + } } diff --git a/src/scenes/MenuScene.cpp b/src/scenes/MenuScene.cpp index 26c690a..6cea3b4 100755 --- a/src/scenes/MenuScene.cpp +++ b/src/scenes/MenuScene.cpp @@ -165,11 +165,6 @@ void MenuScene::onScrollWindowChanged(const ScrollWindowUpdate& update) scrollArrowDown_.setVisible(canScrollTo(update, UINavigationDirection::DOWN)); } -SceneDependencies& MenuScene::getDependencies() -{ - return deps_; -} - void MenuScene::setupMenu() { const VerticalListStyle listStyle = { diff --git a/src/scenes/SceneManager.cpp b/src/scenes/SceneManager.cpp index d210d0e..eb311f1 100755 --- a/src/scenes/SceneManager.cpp +++ b/src/scenes/SceneManager.cpp @@ -5,6 +5,8 @@ #include "scenes/AboutScene.h" #include "scenes/InitTransferPakScene.h" #include "scenes/DistributionPokemonListScene.h" +#include "scenes/SelectFileScene.h" +#include "scenes/DataCopyScene.h" #include @@ -140,6 +142,12 @@ void SceneManager::loadScene() case SceneType::POKETRANSPORTER_GB_REF: scene_ = new PokeTransporterGBRefScene(sceneDeps_, newSceneContext_); break; + case SceneType::SELECT_FILE: + scene_ = new SelectFileScene(sceneDeps_, newSceneContext_); + break; + case SceneType::COPY_DATA: + scene_ = new DataCopyScene(sceneDeps_, newSceneContext_); + break; case SceneType::ABOUT: scene_ = new AboutScene(sceneDeps_, newSceneContext_); break; @@ -147,6 +155,7 @@ void SceneManager::loadScene() break; } + newSceneType_ = SceneType::NONE; if(!scene_) { scene_ = oldScene; @@ -162,7 +171,6 @@ void SceneManager::loadScene() } scene_->init(); - newSceneType_ = SceneType::NONE; } void SceneManager::unloadScene(IScene* scene) diff --git a/src/scenes/SceneWithProgressBar.cpp b/src/scenes/SceneWithProgressBar.cpp new file mode 100644 index 0000000..7403ef4 --- /dev/null +++ b/src/scenes/SceneWithProgressBar.cpp @@ -0,0 +1,50 @@ +#include "scenes/SceneWithProgressBar.h" + +static const Rectangle progressBarBounds = {50, 75, 220, 30}; + +SceneWithProgressBar::SceneWithProgressBar(SceneDependencies& deps) + : SceneWithDialogWidget(deps) + , progressWidget_() +{ +} + +SceneWithProgressBar::~SceneWithProgressBar() +{ +} + +void SceneWithProgressBar::init() +{ + ProgressBarWidgetStyle progressStyle = {0}; + + SceneWithDialogWidget::init(); + + setupProgressBar(progressStyle); + progressWidget_.setStyle(progressStyle); +} + +void SceneWithProgressBar::render(RDPQGraphics& gfx, const Rectangle& sceneBounds) +{ + progressWidget_.render(gfx, sceneBounds); + SceneWithDialogWidget::render(gfx, sceneBounds); +} + +void SceneWithProgressBar::setProgress(double progress) +{ + progressWidget_.setProgress(progress); +} + +void SceneWithProgressBar::setupProgressBar(ProgressBarWidgetStyle& style) +{ + style.bar = { + .margin = { 3, 3, 3, 3 }, + .color = RGBA32(0x0, 0x61, 0xFF, 0xFF) + }; + style.textSettings = { + .fontId = arialId_, + .fontStyleId = fontStyleWhiteId_, + .halign = ALIGN_CENTER, + .valign = VALIGN_CENTER + }; + + progressWidget_.setBounds(progressBarBounds); +} \ No newline at end of file diff --git a/src/scenes/SelectFileScene.cpp b/src/scenes/SelectFileScene.cpp new file mode 100755 index 0000000..3cb237b --- /dev/null +++ b/src/scenes/SelectFileScene.cpp @@ -0,0 +1,220 @@ +#include "scenes/SelectFileScene.h" +#include "scenes/SceneManager.h" +#include "scenes/DataCopyScene.h" + +static const Rectangle titleBounds = {20, 10, 280, 16}; +static const Rectangle fileBrowserBounds = {20, 30, 280, 180}; + +static void fileConfirmedCallback(void* context, const char* path) +{ + auto scene = (SelectFileScene*)context; + scene->onFileConfirmed(path); +} + +static void dialogFinishedCallback(void* context) +{ + auto scene = (SelectFileScene*)context; + scene->onDialogDone(); +} + + +SelectFileScene::SelectFileScene(SceneDependencies& deps, void* context) + : SceneWithDialogWidget(deps) + , fileBrowser_(deps.animationManager) + , fileBrowserFocusSegment_{ + .current = &fileBrowser_ + } + , diag_({0}) + , context_((SelectFileSceneContext*)context) + , dialogWidgetBackgroundSprite_(nullptr) + , uiArrowUpSprite_(nullptr) + , uiArrowDownSprite_(nullptr) + , bButtonPressed_(false) +{ +} + +SelectFileScene::~SelectFileScene() +{ +} + +void SelectFileScene::init() +{ + if(context_->goBackToPreviousSceneInstead) + { + // the context indicates that we should go back to the previous scene instead. + // this is likely because we ended up back here because DataCopyScene is done + // and we want to go back to the backup menu. + deps_.sceneManager.goBackToPreviousScene(); + return; + } + + dialogWidgetBackgroundSprite_ = sprite_load("rom://menu-bg-9slice.sprite"); + uiArrowUpSprite_ = sprite_load("rom://ui-arrow-up.sprite"); + uiArrowDownSprite_ = sprite_load("rom://ui-arrow-down.sprite"); + + SceneWithDialogWidget::init(); + + const FileBrowserWidgetStyle browserStyle = { + .listStyle = { + .background = { + .sprite = dialogWidgetBackgroundSprite_, + .spriteSettings = { + .renderMode = SpriteRenderMode::NINESLICE, + .srcRect = {6, 6, 6, 6} + } + }, + .margin = { + .top = 5, + .bottom = 5 + } + }, + .itemStyle = { + .size = {280, 16}, + .titleNotFocused = { + .fontId = arialId_, + .fontStyleId = fontStyleWhiteId_ + }, + .titleFocused = { + .fontId = arialId_, + .fontStyleId = fontStyleYellowId_ + }, + .leftMargin = 10, + .topMargin = 1 + }, + .scrollArrowUpStyle = { + .image = { + .sprite = uiArrowUpSprite_, + .spriteBounds = Rectangle{0, 0, uiArrowUpSprite_->width, uiArrowUpSprite_->height} + } + }, + .scrollArrowDownStyle = { + .image = { + .sprite = uiArrowDownSprite_, + .spriteBounds = Rectangle{0, 0, uiArrowDownSprite_->width, uiArrowDownSprite_->height} + } + } + }; + + setFocusChain(&fileBrowserFocusSegment_); + fileBrowser_.setBounds(fileBrowserBounds); + fileBrowser_.setStyle(browserStyle); + fileBrowser_.setItemConfirmedCallback(fileConfirmedCallback, this); + fileBrowser_.setFileExtensionToFilter(context_->fileExtensionFilter); + fileBrowser_.setPath((context_->initialPath) ? context_->initialPath : "sd:/"); +} + +void SelectFileScene::destroy() +{ + SceneWithDialogWidget::destroy(); + + if(dialogWidgetBackgroundSprite_) + { + sprite_free(dialogWidgetBackgroundSprite_); + dialogWidgetBackgroundSprite_ = nullptr; + } + + if(uiArrowUpSprite_) + { + sprite_free(uiArrowUpSprite_); + uiArrowUpSprite_ = nullptr; + } + + if(uiArrowDownSprite_) + { + sprite_free(uiArrowDownSprite_); + uiArrowDownSprite_ = nullptr; + } +} + +bool SelectFileScene::handleUserInput(joypad_port_t port, const joypad_inputs_t& inputs) +{ + bool goBackToPreviousSceneOnUnhandledBPress = false; + // keep track of the b button + // if the FileBrowserWidget doesn't handle the release + // then we need to go back to the previous scene + if(!bButtonPressed_ && inputs.btn.b) + { + bButtonPressed_ = true; + } + else if(bButtonPressed_ && !inputs.btn.b) + { + bButtonPressed_ = false; + goBackToPreviousSceneOnUnhandledBPress = true; + } + + bool ret = SceneWithDialogWidget::handleUserInput(port, inputs); + if(ret) + { + return ret; + } + + if(goBackToPreviousSceneOnUnhandledBPress) + { + deps_.sceneManager.goBackToPreviousScene(); + return true; + } + + return false; + // not handled by normal means +} + +void SelectFileScene::render(RDPQGraphics& gfx, const Rectangle& sceneBounds) +{ + fileBrowser_.render(gfx, sceneBounds); + SceneWithDialogWidget::render(gfx, sceneBounds); + + if(context_->titleText && !isZeroSizeRectangle(titleBounds)) + { + const TextRenderSettings renderSettings = { + .fontId = arialId_, + .fontStyleId = fontStyleWhiteId_, + .halign = ALIGN_CENTER + }; + const Rectangle absoluteTextBounds = addOffset(titleBounds, sceneBounds); + gfx.drawText(absoluteTextBounds, context_->titleText, renderSettings); + } +} + +void SelectFileScene::onDialogDone() +{ + deps_.sceneManager.goBackToPreviousScene(); +} + +void SelectFileScene::onFileConfirmed(const char* path) +{ + if(context_->nextScene.type == SceneType::COPY_DATA) + { + auto nextSceneContext = (DataCopySceneContext*)context_->nextScene.context; + nextSceneContext->saveToRestorePath = strdup(path); + } + context_->goBackToPreviousSceneInstead = true; + deps_.sceneManager.switchScene(context_->nextScene.type, context_->nextScene.deleteContextFunc, context_->nextScene.context); +} + +void SelectFileScene::showDialog(DialogData* diagData) +{ + SceneWithDialogWidget::showDialog(diagData); + fileBrowser_.setVisible(false); + setFocusChain(&dialogFocusChainSegment_); +} + + +void SelectFileScene::setupDialog(DialogWidgetStyle& style) +{ + style.background.sprite = dialogWidgetBackgroundSprite_; + style.background.spriteSettings = { + .renderMode = SpriteRenderMode::NINESLICE, + .srcRect = { 6, 6, 6, 6 } + }; + + SceneWithDialogWidget::setupDialog(style); + + dialogWidget_.setOnDialogFinishedCallback(dialogFinishedCallback, this); + dialogWidget_.setVisible(false); +} + +void deleteSelectFileSceneContext(void* context) +{ + auto sceneContext = (SelectFileSceneContext*)context; + delete sceneContext; +} \ No newline at end of file diff --git a/src/scenes/StatsScene.cpp b/src/scenes/StatsScene.cpp index 6d304e0..19e0e36 100644 --- a/src/scenes/StatsScene.cpp +++ b/src/scenes/StatsScene.cpp @@ -119,6 +119,7 @@ void StatsScene::init() menu9SliceSprite_ = sprite_load("rom://menu-bg-9slice.sprite"); fontArialSmallId_ = deps_.fontManager.getFont("rom://Arial-small.font64"); + trainerName = deps_.playerName; SceneWithDialogWidget::init(); @@ -146,7 +147,6 @@ void StatsScene::init() move3Str = getMoveString(static_cast(context_->poke_g1.index_move3)); move4Str = getMoveString(static_cast(context_->poke_g1.index_move4)); pokeName = gen1GameReader_.getPokemonName(pokeIndex); - trainerName = gen1GameReader_.getTrainerName(); shiny = false; snprintf(pokeStatsString_, sizeof(pokeStatsString_), "ATK: %u\nDEF: %u\nSPEC: %u\nSPEED: %u", atk, def, specAtk, speed); break; @@ -167,7 +167,6 @@ void StatsScene::init() move3Str = getMoveString(static_cast(context_->poke_g2.index_move3)); move4Str = getMoveString(static_cast(context_->poke_g2.index_move4)); pokeName = gen2GameReader_.getPokemonName(pokeIndex); - trainerName = gen2GameReader_.getTrainerName(); 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 82cb81c..6141393 100755 --- a/src/scenes/TestScene.cpp +++ b/src/scenes/TestScene.cpp @@ -1,35 +1,29 @@ #include "scenes/TestScene.h" -#include "core/RDPQGraphics.h" -#include "core/FontManager.h" -#include "widget/ImageWidget.h" -#include "widget/TextWidget.h" +#include "scenes/SceneManager.h" -#include +static const Rectangle fileBrowserBounds = {20, 20, 280, 200}; -static const char* tvtypeToString(tv_type_t type) +static void fileConfirmedCallback(void* context, const char* path) { - switch(type) - { - case TV_PAL: - return "PAL"; - case TV_NTSC: - return "NTSC"; - case TV_MPAL: - return "MPAL"; - default: - return "INVALID"; - } + auto scene = (TestScene*)context; + scene->onFileConfirmed(path); } +static void dialogFinishedCallback(void* context) +{ + auto scene = (TestScene*)context; + scene->onDialogDone(); +} + + TestScene::TestScene(SceneDependencies& deps, void*) - : AbstractUIScene(deps) - , scrollWidget_(deps.animationManager) - , scrollWidgetFocusSegment_({ - .current = &scrollWidget_ - }) - , widgets_() - , pokeballSprite_(nullptr) - , oakSprite_(nullptr) + : SceneWithDialogWidget(deps) + , fileBrowser_(deps.animationManager) + , fileBrowserFocusSegment_{ + .current = &fileBrowser_ + } + , diag_({0}) + , dialogWidgetBackgroundSprite_(nullptr) { } @@ -39,128 +33,89 @@ TestScene::~TestScene() void TestScene::init() { - uint8_t widgetType; - const uint8_t fontId = deps_.fontManager.getFont("rom://Arial.font64"); - pokeballSprite_ = sprite_load("rom:/pokeball.sprite"); - oakSprite_ = sprite_load("rom://oak.sprite"); + dialogWidgetBackgroundSprite_ = sprite_load("rom://menu-bg-9slice.sprite"); - debugf("Hello Phil! Your tv type is: %s\r\n", tvtypeToString(get_tv_type())); + SceneWithDialogWidget::init(); - const ScrollWidgetStyle scrollStyle = { - .scrollStep = 15, - .marginRight = 50, - .marginBottom = 50 - }; - - scrollWidget_.setBounds(Rectangle{0, 0, 320, 240}); - scrollWidget_.setStyle(scrollStyle); - scrollWidget_.setFocused(true); - setFocusChain(&scrollWidgetFocusSegment_); - - const Dimensions textDimensions = {.width = 50, .height = 16}; - const Dimensions oakDimensions = {.width = oakSprite_->width, .height = oakSprite_->height }; - const Dimensions pokeballDimensions = {.width = pokeballSprite_->width, .height = pokeballSprite_->height}; - - TextWidgetStyle type1Style = { - .renderSettingsNotFocused = { - .fontId = fontId - } - }; - - ImageWidgetStyle type2Style = { - .image = { - .sprite = oakSprite_, - .spriteBounds = {0, 0, oakDimensions.width, oakDimensions.height} - } - }; - - ImageWidgetStyle type3Style = { - .image = { - .sprite = pokeballSprite_, - .spriteBounds = {0, 0, pokeballDimensions.width, pokeballDimensions.height} - } - }; - - int curXPos = 0; - int curYPos = 0; - int nextYPos = 0; - for(uint8_t i=0; i < 6; ++i) - { - for(uint8_t j=0; j < 6; ++j) - { - widgetType = (j % 3); - - switch(widgetType) - { - case 0: - { - TextWidget* textWidget = new TextWidget(); - textWidget->setStyle(type1Style); - textWidget->setBounds(Rectangle{curXPos, curYPos, textDimensions.width, textDimensions.height}); - textWidget->setData("Hello!"); - scrollWidget_.addWidget(textWidget); - widgets_.push_back(textWidget); - curXPos += textDimensions.width; - if(curYPos + textDimensions.height > nextYPos) - { - nextYPos = curYPos + textDimensions.height; - } - break; + const FileBrowserWidgetStyle browserStyle = { + .listStyle = { + .background = { + .sprite = dialogWidgetBackgroundSprite_, + .spriteSettings = { + .renderMode = SpriteRenderMode::NINESLICE, + .srcRect = {6, 6, 6, 6} } - case 1: - { - ImageWidget* imageWidget = new ImageWidget(); - imageWidget->setStyle(type2Style); - imageWidget->setBounds(Rectangle{curXPos, curYPos, oakDimensions.width, oakDimensions.height}); - scrollWidget_.addWidget(imageWidget); - widgets_.push_back(imageWidget); - curXPos += oakDimensions.width; - if(curYPos + oakDimensions.height > nextYPos) - { - nextYPos = curYPos + oakDimensions.height; - } - break; - } - case 2: - { - ImageWidget* imageWidget = new ImageWidget(); - imageWidget->setStyle(type3Style); - imageWidget->setBounds(Rectangle{curXPos, curYPos, pokeballDimensions.width, pokeballDimensions.height}); - scrollWidget_.addWidget(imageWidget); - widgets_.push_back(imageWidget); - curXPos += pokeballDimensions.width; - if(curYPos + pokeballDimensions.height > nextYPos) - { - nextYPos = curYPos + pokeballDimensions.height; - } - break; - } - default: - break; + }, + .margin = { + .top = 5 } + }, + .itemStyle = { + .size = {280, 16}, + .titleNotFocused = { + .fontId = arialId_, + .fontStyleId = fontStyleWhiteId_ + }, + .titleFocused = { + .fontId = arialId_, + .fontStyleId = fontStyleYellowId_ + }, + .leftMargin = 10, + .topMargin = 1 } - curYPos = nextYPos; - curXPos = 0; - } + }; + + setFocusChain(&fileBrowserFocusSegment_); + fileBrowser_.setBounds(fileBrowserBounds); + fileBrowser_.setStyle(browserStyle); + fileBrowser_.setItemConfirmedCallback(fileConfirmedCallback, this); + fileBrowser_.setFileExtensionToFilter(".bmp"); + fileBrowser_.setPath("sd:/"); } void TestScene::destroy() { - scrollWidget_.clearWidgets(); - for(IWidget* widget : widgets_) - { - delete widget; - } - widgets_.clear(); - - sprite_free(oakSprite_); - oakSprite_ = nullptr; - - sprite_free(pokeballSprite_); - pokeballSprite_ = nullptr; + SceneWithDialogWidget::destroy(); + + sprite_free(dialogWidgetBackgroundSprite_); + dialogWidgetBackgroundSprite_ = nullptr; } void TestScene::render(RDPQGraphics& gfx, const Rectangle& sceneBounds) { - scrollWidget_.render(gfx, sceneBounds); + fileBrowser_.render(gfx, sceneBounds); + SceneWithDialogWidget::render(gfx, sceneBounds); +} + +void TestScene::onDialogDone() +{ + deps_.sceneManager.goBackToPreviousScene(); +} + +void TestScene::onFileConfirmed(const char* path) +{ + setDialogDataText(diag_, "File confirmed: %s", path); + showDialog(&diag_); +} + +void TestScene::showDialog(DialogData* diagData) +{ + SceneWithDialogWidget::showDialog(diagData); + fileBrowser_.setVisible(false); + setFocusChain(&dialogFocusChainSegment_); +} + + +void TestScene::setupDialog(DialogWidgetStyle& style) +{ + style.background.sprite = dialogWidgetBackgroundSprite_; + style.background.spriteSettings = { + .renderMode = SpriteRenderMode::NINESLICE, + .srcRect = { 6, 6, 6, 6 } + }; + + SceneWithDialogWidget::setupDialog(style); + + dialogWidget_.setOnDialogFinishedCallback(dialogFinishedCallback, this); + dialogWidget_.setVisible(false); } \ No newline at end of file diff --git a/src/transferpak/TransferPakDataCopier.cpp b/src/transferpak/TransferPakDataCopier.cpp new file mode 100644 index 0000000..c5c1e1a --- /dev/null +++ b/src/transferpak/TransferPakDataCopier.cpp @@ -0,0 +1,276 @@ +#include "transferpak/TransferPakDataCopier.h" +#include "transferpak/TransferPakRomReader.h" +#include "transferpak/TransferPakSaveManager.h" + +#include + +ITransferPakDataCopySource::~ITransferPakDataCopySource() +{ +} + +ITransferPakDataCopyDestination::~ITransferPakDataCopyDestination() +{ +} + +TransferPakRomReaderCopySource::TransferPakRomReaderCopySource(TransferPakRomReader& romReader) + : romReader_(romReader) + , bytesRead_(0) +{ +} + +TransferPakRomReaderCopySource::~TransferPakRomReaderCopySource() +{ +} + + +bool TransferPakRomReaderCopySource::readyForTransfer() const +{ + return true; +} + +uint16_t TransferPakRomReaderCopySource::getCurrentBankIndex() const +{ + return romReader_.getCurrentBankIndex(); +} + +uint32_t TransferPakRomReaderCopySource::getNumberOfBytesRead() const +{ + return bytesRead_; +} + +uint32_t TransferPakRomReaderCopySource::read(uint8_t* buffer, uint32_t bytesToRead) +{ + if(romReader_.read(buffer, bytesToRead)) + { + bytesRead_ += bytesToRead; + return bytesToRead; + } + return 0; +} + +TransferPakSaveManagerCopySource::TransferPakSaveManagerCopySource(TransferPakSaveManager& saveManager) + : saveManager_(saveManager) + , bytesRead_(0) +{ +} + +TransferPakSaveManagerCopySource::~TransferPakSaveManagerCopySource() +{ +} + +bool TransferPakSaveManagerCopySource::readyForTransfer() const +{ + return true; +} + +uint16_t TransferPakSaveManagerCopySource::getCurrentBankIndex() const +{ + return saveManager_.getCurrentBankIndex(); +} + +uint32_t TransferPakSaveManagerCopySource::getNumberOfBytesRead() const +{ + return bytesRead_; +} + +uint32_t TransferPakSaveManagerCopySource::read(uint8_t* buffer, uint32_t bytesToRead) +{ + if(saveManager_.read(buffer, bytesToRead)) + { + bytesRead_ += bytesToRead; + return bytesToRead; + } + return 0; +} + +TransferPakFileCopySource::TransferPakFileCopySource(const char* filePath) + : inputFile_(nullptr) + , bytesRead_(0) +{ + inputFile_ = fopen(filePath, "r"); +} + +TransferPakFileCopySource::~TransferPakFileCopySource() +{ + if(inputFile_) + { + fclose(inputFile_); + inputFile_ = nullptr; + } +} + +bool TransferPakFileCopySource::readyForTransfer() const +{ + return (inputFile_ != nullptr); +} + +uint16_t TransferPakFileCopySource::getCurrentBankIndex() const +{ + return 1; +} + +uint32_t TransferPakFileCopySource::getNumberOfBytesRead() const +{ + return bytesRead_; +} + +uint32_t TransferPakFileCopySource::read(uint8_t* buffer, uint32_t bytesToRead) +{ + uint32_t ret = static_cast(fread(buffer, sizeof(char), bytesToRead, inputFile_)); + bytesRead_ += ret; + return ret; +} + +TransferPakNullCopySource::TransferPakNullCopySource() + : bytesRead_(0) +{ +} + +TransferPakNullCopySource::~TransferPakNullCopySource() +{ +} + +bool TransferPakNullCopySource::readyForTransfer() const +{ + return true; +} + +uint16_t TransferPakNullCopySource::getCurrentBankIndex() const +{ + return 1; +} + +uint32_t TransferPakNullCopySource::getNumberOfBytesRead() const +{ + return bytesRead_; +} + +uint32_t TransferPakNullCopySource::read(uint8_t *buffer, uint32_t bytesToRead) +{ + memset(buffer, 0, bytesToRead); + return bytesToRead; +} + +TransferPakSaveManagerDestination::TransferPakSaveManagerDestination(TransferPakSaveManager& saveManager) + : saveManager_(saveManager) + , bytesWritten_(0) +{ +} + +TransferPakSaveManagerDestination::~TransferPakSaveManagerDestination() +{ + close(); +} + +bool TransferPakSaveManagerDestination::readyForTransfer() const +{ + return true; +} + +uint16_t TransferPakSaveManagerDestination::getCurrentBankIndex() const +{ + return saveManager_.getCurrentBankIndex(); +} + +uint32_t TransferPakSaveManagerDestination::getNumberOfBytesWritten() const +{ + return bytesWritten_; +} + +uint32_t TransferPakSaveManagerDestination::write(uint8_t* buffer, uint32_t bytesToWrite) +{ + saveManager_.write(buffer, bytesToWrite); + bytesWritten_ += bytesToWrite; + return bytesToWrite; +} + +void TransferPakSaveManagerDestination::close() +{ + // dummy +} + +TransferPakFileCopyDestination::TransferPakFileCopyDestination(const char* pathOnSDCard) + : outputFile_(nullptr) + , bytesWritten_(0) +{ + outputFile_ = fopen(pathOnSDCard, "w"); +} + +TransferPakFileCopyDestination::~TransferPakFileCopyDestination() +{ + close(); +} + +bool TransferPakFileCopyDestination::readyForTransfer() const +{ + return (outputFile_ != nullptr); +} + +uint16_t TransferPakFileCopyDestination::getCurrentBankIndex() const +{ + return 1; +} + +uint32_t TransferPakFileCopyDestination::getNumberOfBytesWritten() const +{ + return bytesWritten_; +} + +uint32_t TransferPakFileCopyDestination::write(uint8_t* buffer, uint32_t bytesToWrite) +{ + const uint32_t ret = static_cast(fwrite(buffer, sizeof(char), bytesToWrite, outputFile_)); + bytesWritten_ += ret; + return ret; +} + +void TransferPakFileCopyDestination::close() +{ + if(outputFile_) + { + fclose(outputFile_); + outputFile_ = nullptr; + } +} + +TransferPakDataCopier::TransferPakDataCopier(ITransferPakDataCopySource& source, ITransferPakDataCopyDestination& destination) + : source_(source) + , destination_(destination) +{ +} + +TransferPakDataCopier::~TransferPakDataCopier() +{ +} + +uint16_t TransferPakDataCopier::getCurrentBankIndex() const +{ + return source_.getCurrentBankIndex(); +} + +uint32_t TransferPakDataCopier::getNumberOfBytesRead() const +{ + return source_.getNumberOfBytesRead(); +} + +size_t TransferPakDataCopier::copyChunk(uint32_t numBytesToCopy) +{ + constexpr uint16_t bufferSize = 256; + uint8_t buffer[bufferSize]; + uint32_t bytesRemaining = numBytesToCopy; + uint32_t bytesToRead; + + while(bytesRemaining > 0) + { + bytesToRead = (bufferSize < bytesRemaining) ? bufferSize : bytesRemaining; + + if(!source_.read(buffer, bytesToRead)) + { + // no bytes read. Abort + break; + } + + // now write the bytes to the destination + bytesRemaining -= destination_.write(buffer, bytesToRead); + } + return numBytesToCopy - bytesRemaining; +} \ No newline at end of file diff --git a/src/transferpak/TransferPakManager.cpp b/src/transferpak/TransferPakManager.cpp index afc120e..da8f629 100755 --- a/src/transferpak/TransferPakManager.cpp +++ b/src/transferpak/TransferPakManager.cpp @@ -100,12 +100,10 @@ uint8_t TransferPakManager::getStatus() return tpak_get_status(static_cast(port_)); } -bool TransferPakManager::validateGbHeader() +bool TransferPakManager::readCartridgeHeader(gameboy_cartridge_header& cartridgeHeader) { - gameboy_cartridge_header header; uint8_t status = getStatus(); int ret; - bool retBool; while(!(status | TPAK_STATUS_READY)) { @@ -119,20 +117,13 @@ bool TransferPakManager::validateGbHeader() return false; } - ret = tpak_get_cartridge_header(static_cast(port_), &header); + ret = tpak_get_cartridge_header(static_cast(port_), &cartridgeHeader); if(ret) { debugf("[TransferPakManager]: ERROR: tpak_get_cartridge_header got error %d\r\n", ret); return false; } - retBool = tpak_check_header(&header); - - if(!retBool) - { - debugf("[TransferPakManager]: ERROR: tpak_check_header returned false!\r\n"); - } - - return retBool; + return true; } void TransferPakManager::switchGBROMBank(uint8_t bankIndex) diff --git a/src/widget/FileBrowserWidget.cpp b/src/widget/FileBrowserWidget.cpp new file mode 100644 index 0000000..a008ecf --- /dev/null +++ b/src/widget/FileBrowserWidget.cpp @@ -0,0 +1,344 @@ +#include "widget/FileBrowserWidget.h" +#include + +static void confirmDirectoryCallback(void* context, const void* itemParam) +{ + auto fileBrowser = (FileBrowserWidget*)context; + fileBrowser->onConfirmDirectory((const char*)itemParam); +} + +static void confirmFileCallback(void* context, const void* itemParam) +{ + auto fileBrowser = (FileBrowserWidget*)context; + fileBrowser->onConfirmFile((const char*)itemParam); +} + +FileBrowserWidget::FileBrowserWidget(AnimationManager& animManager) + : duplicatedDirEntNameList_() + , itemWidgetList_() + , listWidget_(animManager) + , scrollArrowUp_() + , scrollArrowDown_() + , style_({0}) + , status_({ + .itemList = itemWidgetList_, + .err = 0 + }) + , bounds_({0}) + , pathBuffer_() + , onItemConfirmedCallback_(nullptr) + , onItemConfirmedCallbackContext_(nullptr) + , fileExtensionFilter_(nullptr) + , focused_(false) + , visible_(true) + , bButtonPressed_(false) +{ + pathBuffer_[0] = '\0'; + listWidget_.registerScrollWindowListener(this); +} + +FileBrowserWidget::~FileBrowserWidget() +{ + listWidget_.unregisterScrollWindowListener(this); + clearList(); +} + +bool FileBrowserWidget::isFocused() const +{ + return focused_; +} + +void FileBrowserWidget::setFocused(bool focused) +{ + focused_ = focused; + listWidget_.setFocused(focused); +} + +bool FileBrowserWidget::isVisible() const +{ + return visible_; +} + +void FileBrowserWidget::setVisible(bool visible) +{ + visible_ = visible; +} + +Rectangle FileBrowserWidget::getBounds() const +{ + return bounds_; +} + +void FileBrowserWidget::setBounds(const Rectangle& bounds) +{ + bounds_ = bounds; + listWidget_.setBounds(Rectangle{0, 0, bounds.width, bounds.height}); + + scrollArrowUp_.setBounds(Rectangle{bounds.width / 2, -6, style_.scrollArrowUpStyle.image.spriteBounds.width, style_.scrollArrowUpStyle.image.spriteBounds.height}); + scrollArrowDown_.setBounds(Rectangle{bounds.width / 2, bounds.height, style_.scrollArrowDownStyle.image.spriteBounds.width, style_.scrollArrowDownStyle.image.spriteBounds.height}); +} + +Dimensions FileBrowserWidget::getSize() const +{ + return Dimensions{bounds_.width, bounds_.height}; +} + +void FileBrowserWidget::setStyle(const FileBrowserWidgetStyle& style) +{ + const Rectangle bounds = getBounds(); + + style_ = style; + listWidget_.setStyle(style.listStyle); + + scrollArrowUp_.setStyle(style.scrollArrowUpStyle); + scrollArrowUp_.setBounds(Rectangle{bounds.width / 2, -6, style.scrollArrowUpStyle.image.spriteBounds.width, style.scrollArrowUpStyle.image.spriteBounds.height}); + + // note: even though autogrow is turned on for the vertical list, it doesn't matter for the down arrow. + // because when the list is still growing, no scrolling is needed anyway, so the arrow would be invisible anyway. + scrollArrowDown_.setStyle(style.scrollArrowDownStyle); + scrollArrowDown_.setBounds(Rectangle{bounds.width / 2, bounds.height, style.scrollArrowDownStyle.image.spriteBounds.width, style.scrollArrowDownStyle.image.spriteBounds.height}); +} + +bool FileBrowserWidget::handleUserInput(const joypad_inputs_t& userInput) +{ + bool handled = listWidget_.handleUserInput(userInput); + + if(!handled) + { + if(userInput.btn.b) + { + if(!bButtonPressed_) + { + bButtonPressed_ = true; + handled = true; + } + } + else + { + if(bButtonPressed_) + { + bButtonPressed_ = false; + handled = goToParentDirectory(); + } + } + } + return handled; +} + +void FileBrowserWidget::render(RDPQGraphics& gfx, const Rectangle& parentBounds) +{ + const Rectangle absoluteBounds = addOffset(bounds_, parentBounds); + + listWidget_.render(gfx, absoluteBounds); + scrollArrowUp_.render(gfx, absoluteBounds); + scrollArrowDown_.render(gfx, absoluteBounds); +} + +const FileBrowserWidgetStatus& FileBrowserWidget::getStatus() const +{ + return status_; +} + +const char* FileBrowserWidget::getPath() const +{ + return pathBuffer_; +} + +void FileBrowserWidget::setPath(const char* path) +{ + strncpy(pathBuffer_, path, sizeof(pathBuffer_) - 1); + + clearList(); + loadDirectoryItems(); +} + +void FileBrowserWidget::onConfirmDirectory(const char* path) +{ + size_t pathLength = strnlen(pathBuffer_, sizeof(pathBuffer_)); + if(pathBuffer_[pathLength - 1] != '/') + { + pathBuffer_[pathLength] = '/'; + ++pathLength; + } + strncpy(pathBuffer_ + pathLength, path, sizeof(pathBuffer_) - 1 - pathLength); + + // reload items + clearList(); + loadDirectoryItems(); +} + +void FileBrowserWidget::onConfirmFile(const char* path) +{ + char fullPath[sizeof(pathBuffer_)]; + size_t pathLength = strnlen(pathBuffer_, sizeof(pathBuffer_)); + + memcpy(fullPath, pathBuffer_, sizeof(pathBuffer_)); + + if(fullPath[pathLength - 1] != '/') + { + fullPath[pathLength] = '/'; + ++pathLength; + } + strncpy(fullPath + pathLength, path, sizeof(fullPath) - 1 - pathLength); + + if(onItemConfirmedCallback_) + { + onItemConfirmedCallback_(onItemConfirmedCallbackContext_, fullPath); + } +} + +void FileBrowserWidget::setItemConfirmedCallback(void (*onItemConfirmed)(void*, const char*), void* context) +{ + onItemConfirmedCallback_ = onItemConfirmed; + onItemConfirmedCallbackContext_ = context; +} + +void FileBrowserWidget::setFileExtensionToFilter(const char* fileExtensionFilter) +{ + fileExtensionFilter_ = fileExtensionFilter; + + // reload items + clearList(); + loadDirectoryItems(); +} + +void FileBrowserWidget::onScrollWindowChanged(const ScrollWindowUpdate& update) +{ + scrollArrowUp_.setVisible(canScrollTo(update, UINavigationDirection::UP)); + scrollArrowDown_.setVisible(canScrollTo(update, UINavigationDirection::DOWN)); +} + +void FileBrowserWidget::clearList() +{ + listWidget_.clearWidgets(); + for(MenuItemWidget* widget : itemWidgetList_) + { + delete widget; + } + itemWidgetList_.clear(); + + for(char* dirEntName : duplicatedDirEntNameList_) + { + free(dirEntName); + } + duplicatedDirEntNameList_.clear(); +} + +void FileBrowserWidget::loadDirectoryItems() +{ + MenuItemWidget* itemWidget; + MenuItemData itemData; + MenuItemStyle itemStyle = style_.itemStyle; + dir_t dirEnt; + int ret; + char* titleString; + size_t fileExtensionFilterLength; + size_t dirNameLength; + + if(pathBuffer_[0] == '\0') + { + // empty path. don't do anything + return; + } + + ret = dir_findfirst(pathBuffer_, &dirEnt); + if(ret != 0) + { + status_.err = errno; + return; + } + + fileExtensionFilterLength = (fileExtensionFilter_) ? strlen(fileExtensionFilter_) : 0; + + while(ret == 0) + { + dirNameLength = strnlen(dirEnt.d_name, sizeof(dirEnt.d_name)); + + // apply file extension filter if one has been specified + if(dirEnt.d_type == DT_REG && fileExtensionFilter_ && dirNameLength > fileExtensionFilterLength) + { + if(strncmp(dirEnt.d_name + dirNameLength - fileExtensionFilterLength, fileExtensionFilter_, fileExtensionFilterLength)) + { + // file extension doesn't matching, discard result + ret = dir_findnext(pathBuffer_, &dirEnt); + continue; + } + } + // libdragon overwrites a dir_t instance on every dir_findnext call + // and the dir_t instance has a static allocated d_name entry + // which means it gets overwritten on every dir_findnext() call. + // so to avoid losing the name, we need to duplicate it. + titleString = strdup(dirEnt.d_name); + // we also need to track this duplicated string in order to free() it when done + duplicatedDirEntNameList_.push_back(titleString); + + itemData.title = titleString; + itemData.onConfirmAction = (dirEnt.d_type == DT_REG) ? confirmFileCallback : confirmDirectoryCallback; + itemData.context = this; + itemData.itemParam = titleString; + + itemWidget = new MenuItemWidget(); + itemWidget->setData(itemData); + itemWidget->setStyle(itemStyle); + listWidget_.addWidget(itemWidget); + // we are responsible for delete'ing the MenuItemWidget instance + // so we need to keep track of it. + itemWidgetList_.push_back(itemWidget); + + ret = dir_findnext(pathBuffer_, &dirEnt); + } +} + +bool FileBrowserWidget::goToParentDirectory() +{ + bool gotMoreThanOneForwardSlash = false; + const size_t pathLength = strlen(pathBuffer_); + + // figure out the last forward slash offset AND whether there's more than one forward slash + char* cur = pathBuffer_ + pathLength; + char* lastSlash = nullptr; + while(cur >= pathBuffer_) + { + if(*cur == '/') + { + if(!lastSlash) + { + lastSlash = cur; + } + else + { + gotMoreThanOneForwardSlash = true; + break; + } + } + --cur; + } + + if(!gotMoreThanOneForwardSlash) + { + // We're either in the first subdirectory level (example: sd:/Wallpapers) + // OR we're already in the root dir (sd:/) + if(pathBuffer_[pathLength - 1] == '/') + { + // already in root dir + return false; + } + else + { + // in 1st level subdir + // finish path string directly after the forward slash + *(lastSlash + 1) = '\0'; + } + } + else + { + // subdir deeper than 1st level: (example: sd:/Music/Europe) + // replace the last forward slash with a 0 character to end the string there + *lastSlash = '\0'; + } + + // now reload the FileBrowserWidget items. + clearList(); + loadDirectoryItems(); + return true; +} \ No newline at end of file diff --git a/src/widget/MenuItemWidget.cpp b/src/widget/MenuItemWidget.cpp index 7519431..80baa26 100755 --- a/src/widget/MenuItemWidget.cpp +++ b/src/widget/MenuItemWidget.cpp @@ -14,6 +14,11 @@ MenuItemWidget::~MenuItemWidget() { } +const MenuItemData& MenuItemWidget::getData() const +{ + return data_; +} + void MenuItemWidget::setData(const MenuItemData& data) { data_ = data; diff --git a/src/widget/ProgressBarWidget.cpp b/src/widget/ProgressBarWidget.cpp new file mode 100644 index 0000000..de880ce --- /dev/null +++ b/src/widget/ProgressBarWidget.cpp @@ -0,0 +1,117 @@ +#include "widget/ProgressBarWidget.h" + +ProgressBarWidget::ProgressBarWidget() + : style_({0}) + , visible_(true) + , bounds_({0}) + , progress_() + , textBuffer_() +{ + setProgress(0); +} + +ProgressBarWidget::~ProgressBarWidget() +{ +} + +void ProgressBarWidget::setProgress(double progress) +{ + progress_ = progress; + + snprintf(textBuffer_, sizeof(textBuffer_), "%hu%%", static_cast(progress * 100.0)); +} + +void ProgressBarWidget::setStyle(const ProgressBarWidgetStyle& style) +{ + style_ = style; +} + +bool ProgressBarWidget::isFocused() const +{ + return false; +} + +void ProgressBarWidget::setFocused(bool) +{ + //dummy +} + +bool ProgressBarWidget::isVisible() const +{ + return visible_; +} + +void ProgressBarWidget::setVisible(bool visible) +{ + visible_ = visible; +} + +Rectangle ProgressBarWidget::getBounds() const +{ + return bounds_; +} + +void ProgressBarWidget::setBounds(const Rectangle& bounds) +{ + bounds_ = bounds; +} + +Dimensions ProgressBarWidget::getSize() const +{ + return Dimensions{bounds_.width, bounds_.height}; +} + +bool ProgressBarWidget::handleUserInput(const joypad_inputs_t&) +{ + return false; +} + +void ProgressBarWidget::render(RDPQGraphics& gfx, const Rectangle& parentBounds) +{ + if(!visible_) + { + return; + } + + const Rectangle myAbsoluteBounds = addOffset(bounds_, parentBounds); + const Rectangle foregroundRectangle = { + .x = myAbsoluteBounds.x + style_.bar.margin.left, + .y = myAbsoluteBounds.y + style_.bar.margin.top, + .width = bounds_.width - style_.bar.margin.left - style_.bar.margin.right, + .height = bounds_.height - style_.bar.margin.top - style_.bar.margin.bottom + }; + + // draw background + if(style_.background.sprite) + { + gfx.drawSprite(myAbsoluteBounds, style_.background.sprite, style_.background.renderSettings); + } + + // draw foreground + if(progress_) + { + const Rectangle progressRectangle = { + .x = foregroundRectangle.x, + .y = foregroundRectangle.y, + .width = static_cast(foregroundRectangle.width * progress_), + .height = foregroundRectangle.height + }; + + if(progressRectangle.width && progressRectangle.height) + { + if(style_.bar.sprite) + { + gfx.drawSprite(progressRectangle, style_.bar.sprite, style_.bar.spriteSettings); + } + else + { + gfx.fillRectangle(progressRectangle, style_.bar.color); + } + } + } + + if(style_.textSettings.fontId) + { + gfx.drawText(foregroundRectangle, textBuffer_, style_.textSettings); + } +} \ No newline at end of file diff --git a/src/widget/TransferPakDetectionWidget.cpp b/src/widget/TransferPakDetectionWidget.cpp index 1d4b7f4..3d11a6f 100755 --- a/src/widget/TransferPakDetectionWidget.cpp +++ b/src/widget/TransferPakDetectionWidget.cpp @@ -2,6 +2,9 @@ #include "transferpak/TransferPakManager.h" #include "transferpak/TransferPakRomReader.h" #include "transferpak/TransferPakSaveManager.h" +#include "gen1/Gen1GameReader.h" +#include "gen2/Gen2GameReader.h" +#include "tpak.h" /** * @brief This function allows you to specify a 32 bit RGBA color by specifying separate color components @@ -27,20 +30,6 @@ static const uint16_t paletteGold[] = {0, colorToRGBA16(0x8A, 0x86, 0x48, 0xFF), static const uint16_t paletteSilver[] = {0, colorToRGBA16(0x90, 0x8D, 0x85, 0xFF), colorToRGBA16(0xA2, 0x9E, 0x98, 0xFF), 0, 0, 0, 0, 0}; static const uint16_t paletteCrystal[] = {0, colorToRGBA16(0x55, 0x7A, 0x77, 0xFF), colorToRGBA16(0x72, 0x9E, 0xA4, 0xFF), 0, 0, 0, 0, 0}; -#if 0 -#include "gen2/Gen2GameReader.h" -static void doRandomShit(TransferPakManager& tpakManager) -{ - TransferPakRomReader romReader(tpakManager); - TransferPakSaveManager saveManager(tpakManager); - Gen2GameReader reader(romReader, saveManager, Gen2GameType::CRYSTAL); - tpakManager.setRAMEnabled(true); - debugf("first pokemon: %s\r\n", reader.getPokemonName(1)); - - debugf("Trainer name: %s\r\n", reader.getTrainerName()); -} -#endif - TransferPakDetectionWidget::TransferPakDetectionWidget(AnimationManager& animManager, TransferPakManager& pakManager) : style_({0}) , animManager_(animManager) @@ -112,22 +101,33 @@ Dimensions TransferPakDetectionWidget::getSize() const bool TransferPakDetectionWidget::handleUserInput(const joypad_inputs_t& userInput) { - bool ret = false; + bool handled = false; if(previousInputState_.btn.a && !userInput.btn.a) { switch(currentState_) { case TransferPakWidgetState::UNKNOWN: switchState(currentState_, TransferPakWidgetState::DETECTING_PAK); - ret = true; + handled = true; break; default: break; } } + else if(currentState_ == TransferPakWidgetState::VALIDATING_GAME_SAVE) + { + // We don't want to do this in the switchState flow in order to have the widget actually render something before starting this step + // (because validating the game save CRC might take a few seconds) + tpakManager_.setRAMEnabled(true); + const bool ret = validateGameSave(); + tpakManager_.setRAMEnabled(false); + const TransferPakWidgetState newState = (ret) ? TransferPakWidgetState::VALID_SAVE_FOUND : TransferPakWidgetState::NO_SAVE_FOUND; + + switchState(currentState_, newState); + } previousInputState_ = userInput; - return ret; + return handled; } void TransferPakDetectionWidget::render(RDPQGraphics& gfx, const Rectangle& parentBounds) @@ -203,7 +203,8 @@ void TransferPakDetectionWidget::switchState(TransferPakWidgetState previousStat return; case TransferPakWidgetState::GAME_FOUND: updateCartridgeIcon(); -// doRandomShit(tpakManager_); + newState = TransferPakWidgetState::VALIDATING_GAME_SAVE; + switchState(state, newState); break; default: break; @@ -222,6 +223,12 @@ void TransferPakDetectionWidget::renderUnknownState(RDPQGraphics& gfx, const Rec gfx.drawText(absoluteTextBounds, "Press A to start", style_.textSettings); } +void TransferPakDetectionWidget::renderValidatingSaveState(RDPQGraphics& gfx, const Rectangle& parentBounds) +{ + const Rectangle absoluteTextBounds = addOffset(textBounds, bounds_); + gfx.drawText(absoluteTextBounds, "Checking save...", style_.textSettings); +} + void TransferPakDetectionWidget::renderErrorState(RDPQGraphics& gfx, const Rectangle& parentBounds) { const Rectangle absoluteTextBounds = addOffset(textBounds, bounds_); @@ -268,11 +275,25 @@ bool TransferPakDetectionWidget::selectTransferPak() bool TransferPakDetectionWidget::validateGameboyHeader() { + gameboy_cartridge_header cartridgeHeader; if(!tpakManager_.setPower(true)) { return false; } - return tpakManager_.validateGbHeader(); + + + if(!tpakManager_.readCartridgeHeader(cartridgeHeader)) + { + return false; + } + + if(!tpak_check_header(&cartridgeHeader)) + { + debugf("[TransferPakDetectionWidget]: ERROR: tpak_check_header returned false!\r\n"); + return false; + } + + return true; } bool TransferPakDetectionWidget::detectGameType() @@ -346,4 +367,23 @@ void TransferPakDetectionWidget::updateCartridgeIcon() } cartridgeLabelSprite_ = sprite_load(labelSpritePath); } +} + +bool TransferPakDetectionWidget::validateGameSave() +{ + TransferPakRomReader romReader(tpakManager_); + TransferPakSaveManager saveManager(tpakManager_); + if(gen1Type_ != Gen1GameType::INVALID) + { + Gen1GameReader gen1Reader(romReader, saveManager, gen1Type_); + + return gen1Reader.isMainChecksumValid(); + } + else if(gen2Type_ != Gen2GameType::INVALID) + { + Gen2GameReader gen2Reader(romReader, saveManager, gen2Type_); + + return gen2Reader.isMainChecksumValid(); + } + return false; } \ No newline at end of file diff --git a/src/widget/VerticalList.cpp b/src/widget/VerticalList.cpp index c3eb769..57c1a36 100755 --- a/src/widget/VerticalList.cpp +++ b/src/widget/VerticalList.cpp @@ -140,7 +140,7 @@ bool VerticalList::focusNext() changeStatus.curFocus = widgetList_[focusedWidgetIndex_]; changeStatus.focusBounds = calculateListWidgetBounds(widgetBoundsList_[focusedWidgetIndex_], windowMinY_, bounds_.x + listStyle_.margin.left, bounds_.y + listStyle_.margin.top); - widgetList_[focusedWidgetIndex_]->setFocused(true); + widgetList_[focusedWidgetIndex_]->setFocused(focused_); const int32_t scrollAmountY = scrollWindowToFocusedWidget(); changeStatus.focusBounds.y -= scrollAmountY; @@ -168,7 +168,7 @@ bool VerticalList::focusPrevious() changeStatus.curFocus = widgetList_[focusedWidgetIndex_]; changeStatus.focusBounds = calculateListWidgetBounds(widgetBoundsList_[focusedWidgetIndex_], windowMinY_, bounds_.x + listStyle_.margin.left, bounds_.y + listStyle_.margin.top); - widgetList_[focusedWidgetIndex_]->setFocused(true); + widgetList_[focusedWidgetIndex_]->setFocused(focused_); const int32_t scrollAmountY = scrollWindowToFocusedWidget(); changeStatus.focusBounds.y -= scrollAmountY; @@ -246,6 +246,7 @@ void VerticalList::clearWidgets() widgetList_.clear(); widgetBoundsList_.clear(); focusedWidgetIndex_ = 0; + windowMinY_ = 0; notifyScrollWindowListeners(); }