From e51726eef94e456003cb43c3db1a994325a44664 Mon Sep 17 00:00:00 2001 From: Philippe Symons Date: Fri, 19 Jul 2024 21:46:11 +0200 Subject: [PATCH] Initial import of the first (extremely early) functional version. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It doesn't look like much, but it's functional ;-) It can inject distribution pokémon into gen 1 and gen 2 original cartridges and inject the GS ball in Pokémon Crystal. That's it. But that was the minimal feature set I had in mind for the project. In the Readme.md you can find the ideas I have for expanding the project. But the first priority is the UI, because it really looks bad right now. (as I was mostly focused on building blocks and transfer pak functionality. Not on making it looks good) --- .gitmodules | 6 + CREDITS.md | 16 + LICENSE | 218 +-------- Makefile | 57 +++ README.md | 73 +++ assets/Arial.ttf | Bin 0 -> 275572 bytes assets/hand-cursor.png | Bin 0 -> 4415 bytes assets/menu-bg-9slice.png | Bin 0 -> 359 bytes assets/oak.png | Bin 0 -> 5508 bytes assets/pokeball.png | Bin 0 -> 600 bytes include/animations/AnimationManager.h | 26 ++ include/animations/IAnimation.h | 144 ++++++ include/animations/MoveAnimation.h | 37 ++ include/core/Application.h | 29 ++ include/core/DragonUtils.h | 29 ++ include/core/FontManager.h | 49 ++ include/core/RDPQGraphics.h | 77 +++ include/core/Sprite.h | 63 +++ include/core/common.h | 35 ++ include/menu/MenuEntries.h | 15 + include/menu/MenuFunctions.h | 15 + include/scenes/AbstractUIScene.h | 83 ++++ include/scenes/DistributionPokemonListScene.h | 83 ++++ include/scenes/IScene.h | 55 +++ include/scenes/InitTransferPakScene.h | 44 ++ include/scenes/MenuScene.h | 68 +++ include/scenes/SceneManager.h | 72 +++ include/scenes/SceneWithDialogWidget.h | 28 ++ include/scenes/TestScene.h | 36 ++ include/transferpak/TransferPakManager.h | 103 ++++ include/transferpak/TransferPakRomReader.h | 64 +++ include/transferpak/TransferPakSaveManager.h | 62 +++ include/widget/CursorWidget.h | 69 +++ include/widget/DialogWidget.h | 118 +++++ include/widget/IFocusListener.h | 49 ++ include/widget/IWidget.h | 75 +++ include/widget/ListItemFiller.h | 53 +++ include/widget/MenuItemWidget.h | 123 +++++ include/widget/TransferPakDetectionWidget.h | 148 ++++++ include/widget/VerticalList.h | 162 +++++++ libdragon | 1 + libpokemegb | 1 + src/animations/AnimationManager.cpp | 37 ++ src/animations/IAnimation.cpp | 117 +++++ src/animations/MoveAnimation.cpp | 52 +++ src/core/Application.cpp | 59 +++ src/core/DragonUtils.cpp | 48 ++ src/core/FontManager.cpp | 36 ++ src/core/RDPQGraphics.cpp | 299 ++++++++++++ src/core/common.cpp | 16 + src/main.cpp | 9 + src/menu/MenuEntries.cpp | 41 ++ src/menu/MenuFunctions.cpp | 120 +++++ src/scenes/AbstractUIScene.cpp | 97 ++++ src/scenes/DistributionPokemonListScene.cpp | 259 +++++++++++ src/scenes/IScene.cpp | 5 + src/scenes/InitTransferPakScene.cpp | 221 +++++++++ src/scenes/MenuScene.cpp | 229 +++++++++ src/scenes/SceneManager.cpp | 144 ++++++ src/scenes/SceneWithDialogWidget.cpp | 67 +++ src/scenes/TestScene.cpp | 93 ++++ src/transferpak/TransferPakManager.cpp | 299 ++++++++++++ src/transferpak/TransferPakRomReader.cpp | 98 ++++ src/transferpak/TransferPakSaveManager.cpp | 118 +++++ src/widget/CursorWidget.cpp | 130 ++++++ src/widget/DialogWidget.cpp | 192 ++++++++ src/widget/IFocusListener.cpp | 5 + src/widget/MenuItemWidget.cpp | 110 +++++ src/widget/TransferPakDetectionWidget.cpp | 241 ++++++++++ src/widget/VerticalList.cpp | 439 ++++++++++++++++++ 70 files changed, 5770 insertions(+), 197 deletions(-) create mode 100644 .gitmodules create mode 100755 CREDITS.md mode change 100644 => 100755 LICENSE create mode 100755 Makefile create mode 100755 README.md create mode 100755 assets/Arial.ttf create mode 100755 assets/hand-cursor.png create mode 100755 assets/menu-bg-9slice.png create mode 100755 assets/oak.png create mode 100755 assets/pokeball.png create mode 100755 include/animations/AnimationManager.h create mode 100755 include/animations/IAnimation.h create mode 100755 include/animations/MoveAnimation.h create mode 100755 include/core/Application.h create mode 100755 include/core/DragonUtils.h create mode 100755 include/core/FontManager.h create mode 100755 include/core/RDPQGraphics.h create mode 100755 include/core/Sprite.h create mode 100755 include/core/common.h create mode 100755 include/menu/MenuEntries.h create mode 100755 include/menu/MenuFunctions.h create mode 100755 include/scenes/AbstractUIScene.h create mode 100755 include/scenes/DistributionPokemonListScene.h create mode 100755 include/scenes/IScene.h create mode 100755 include/scenes/InitTransferPakScene.h create mode 100755 include/scenes/MenuScene.h create mode 100755 include/scenes/SceneManager.h create mode 100755 include/scenes/SceneWithDialogWidget.h create mode 100755 include/scenes/TestScene.h create mode 100755 include/transferpak/TransferPakManager.h create mode 100755 include/transferpak/TransferPakRomReader.h create mode 100755 include/transferpak/TransferPakSaveManager.h create mode 100755 include/widget/CursorWidget.h create mode 100755 include/widget/DialogWidget.h create mode 100755 include/widget/IFocusListener.h create mode 100755 include/widget/IWidget.h create mode 100755 include/widget/ListItemFiller.h create mode 100755 include/widget/MenuItemWidget.h create mode 100755 include/widget/TransferPakDetectionWidget.h create mode 100755 include/widget/VerticalList.h create mode 160000 libdragon create mode 160000 libpokemegb create mode 100755 src/animations/AnimationManager.cpp create mode 100755 src/animations/IAnimation.cpp create mode 100755 src/animations/MoveAnimation.cpp create mode 100755 src/core/Application.cpp create mode 100755 src/core/DragonUtils.cpp create mode 100755 src/core/FontManager.cpp create mode 100755 src/core/RDPQGraphics.cpp create mode 100755 src/core/common.cpp create mode 100755 src/main.cpp create mode 100755 src/menu/MenuEntries.cpp create mode 100755 src/menu/MenuFunctions.cpp create mode 100755 src/scenes/AbstractUIScene.cpp create mode 100755 src/scenes/DistributionPokemonListScene.cpp create mode 100755 src/scenes/IScene.cpp create mode 100755 src/scenes/InitTransferPakScene.cpp create mode 100755 src/scenes/MenuScene.cpp create mode 100755 src/scenes/SceneManager.cpp create mode 100755 src/scenes/SceneWithDialogWidget.cpp create mode 100755 src/scenes/TestScene.cpp create mode 100755 src/transferpak/TransferPakManager.cpp create mode 100755 src/transferpak/TransferPakRomReader.cpp create mode 100755 src/transferpak/TransferPakSaveManager.cpp create mode 100755 src/widget/CursorWidget.cpp create mode 100755 src/widget/DialogWidget.cpp create mode 100755 src/widget/IFocusListener.cpp create mode 100755 src/widget/MenuItemWidget.cpp create mode 100755 src/widget/TransferPakDetectionWidget.cpp create mode 100755 src/widget/VerticalList.cpp diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..a3f0836 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "libdragon"] + path = libdragon + url = git@github.com:DragonMinded/libdragon.git +[submodule "libpokemegb"] + path = libpokemegb + url = git@github.com:risingPhil/libpokemegb.git diff --git a/CREDITS.md b/CREDITS.md new file mode 100755 index 0000000..9ac3e76 --- /dev/null +++ b/CREDITS.md @@ -0,0 +1,16 @@ +# Credits + +First of all, I want to thank the people behind [Bulbapedia](https://bulbapedia.bulbagarden.net/wiki/Main_Page). The documentation was extensive and without it, I never would've started on this project. + +Secondly, I'd like to extend my thanks to Alex "IsoFrieze" Losego from the [Retro Game Mechanics Explained](https://www.youtube.com/c/retrogamemechanicsexplained) Youtube channel. His videos on the topic of Gen 1 sprite decompression were pretty helpful. + +I don't know the identity of the authors of the Pokémon gen 1 and 2 ROM maps on http://datacrystal.romhacking.net, but these were a valuable resource as well. + +I also want to thank the community behind the [pokecrystal](https://github.com/pret/pokecrystal/) project. Without them, I wouldn't as easily have found the rom offsets of the pokémon front sprites for Pokémon Crystal. + +Furthermore I like to develop the developers of the [libdragon](https://github.com/DragonMinded/libdragon) project as this project likely wouldn't have been feasible without it. I especially want to thank [Rasky](https://github.com/rasky) who helped me with some issues I encountered during development. + +And of course I'd like to extend my thanks to everyone else in the pokémunity who wrote documentation or code for these old pokémon games. + +I've listed the specific resources I used for reading the rom/save game data in the CREDITS.md file of the libpokemegb project. + diff --git a/LICENSE b/LICENSE old mode 100644 new mode 100755 index 261eeb9..1c72d78 --- a/LICENSE +++ b/LICENSE @@ -1,201 +1,25 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ + Copyright © 2024 PokeMe64. All rights reserved. - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: - 1. Definitions. + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the + distribution. - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/Makefile b/Makefile new file mode 100755 index 0000000..f2a14a7 --- /dev/null +++ b/Makefile @@ -0,0 +1,57 @@ +V=1 +SOURCE_DIR=src +BUILD_DIR=build +include $(N64_INST)/include/n64.mk + +# the -fno-rtti option is necessary here because libpokemegb defines it too. Otherwise you get an "undefined reference to typeinfo" link error +# when using libpokemegb classes. Source: https://stackoverflow.com/questions/11904519/c-what-are-the-causes-of-undefined-reference-to-typeinfo-for-class-name +N64_C_AND_CXX_FLAGS += -I include -I libpokemegb/include -fno-rtti -fno-exceptions +N64_LDFLAGS += -Llibpokemegb -lpokemegb +N64_ROM_REGIONFREE=1 +N64_ROM_CONTROLLER_TYPE1=n64,pak=transfer +N64_ROM_TITLE="PokeMe64" + +SRCS := $(shell find $(SOURCE_DIR) -type f -name '*.cpp') +OBJS := $(patsubst $(SOURCE_DIR)/%.cpp,$(BUILD_DIR)/%.o,$(SRCS)) + +assets_ttf = $(wildcard assets/*.ttf) +assets_png = $(wildcard assets/*.png) + +assets_conv = $(addprefix filesystem/,$(notdir $(assets_ttf:%.ttf=%.font64))) \ + $(addprefix filesystem/,$(notdir $(assets_png:%.png=%.sprite))) + +MKSPRITE_FLAGS ?= +MKFONT_FLAGS ?= + +all: PokeMe64.z64 +.PHONY: all + +filesystem/%.font64: assets/%.ttf + @mkdir -p $(dir $@) + @echo " [FONT] $@" + $(N64_MKFONT) $(MKFONT_FLAGS) -o filesystem "$<" + +filesystem/%.sprite: assets/%.png + @mkdir -p $(dir $@) + @echo " [SPRITE] $@" + $(N64_MKSPRITE) $(MKSPRITE_FLAGS) -o filesystem "$<" + +filesystem/Arial.font64: MKFONT_FLAGS+=--size 11 --outline 1.0 --char-spacing 1.0 --range 20-E9 + +filesystem/menu-bg-9slice.sprite: MKSPRITE_FLAGS += -f RGBA16 + +pokemegb: + $(MAKE) -C libpokemegb CC=$(N64_CC) CXX=$(N64_CXX) LD=$(N64_LD) PNG_SUPPORT=0 libpokemegb.a + +$(BUILD_DIR)/PokeMe64.dfs: $(assets_conv) +$(BUILD_DIR)/PokeMe64.elf: $(OBJS) pokemegb + +PokeMe64.z64: $(BUILD_DIR)/PokeMe64.dfs + +clean: + $(MAKE) -C libpokemegb CC=$(N64_CC) CXX=$(N64_CXX) LD=$(N64_LD) PNG_SUPPORT=0 clean + rm -rf $(BUILD_DIR) *.z64 + rm -rf filesystem/* +.PHONY: clean + +-include $(wildcard $(BUILD_DIR)/*.d) \ No newline at end of file diff --git a/README.md b/README.md new file mode 100755 index 0000000..4af05e8 --- /dev/null +++ b/README.md @@ -0,0 +1,73 @@ +# Introduction + +This project lets you acquire past Distribution Event Pokémon in Gen1/Gen2 Pokémon gameboy games specifically using a Nintendo 64 with a Transfer Pak. You can run it by using a flashcart, such as an Everdrive64, ED64Plus and others. + +The rom is based on [libpokemegb](https://github.com/risingPhil/libpokemegb) and [libdragon](https://github.com/DragonMinded/libdragon). + +Right now the UI is extremely barebones. I aim to improve this in future versions. + +I'm happy to accept pull requests if the community wants to do them. + +# Current Features +- Inject Generation 1 Distribution event Pokémon into Gen 1 cartridges. In practice, this just means all kinds of variants of Mew. +- Inject Generation 2 Distribution event Pokémon into Gen 2 cartridges. This includes the Pokémon Center New York (PCNY) Distribution event ones. +- Inject GS Ball into an actual Pokémon Crystal gameboy cartridge + +# Limitations +- Right now, this library only supports the international (English) versions of the games. (as far as I know) + +# Build + +To build it, set up a [build environment for libdragon](https://github.com/DragonMinded/libdragon/wiki/Installing-libdragon). + +\#Build with 12 threads (Change the thread number to what you want): + + libdragon make -j 12 + +# 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! + +- Copy PokeMe64.z64 to your Nintendo 64 flash cartridge. (such as Everdrive64, Super 64, ED64Plus, ...) +- 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 +- Now power on your Nintendo 64 +- Load the PokeMe64.z64 rom +- Follow the instructions + +# Goal +This project was created to preserve/improve access to the original Distribution event pokémon from Gen1 and Gen2 for actual Pokémon gameboy cartridges. You could kinda do this with a gb operator and a pc. +But having it done with a Nintendo 64 feels more "real"/"official" and is easier if you have the console and Transfer pak. + +# Future potential improvements (ideas) +- UI: Make the initial transfer pack detect screen show the gameboy cartridge image of the game that was detected and some kind of icon when there's an error. +- UI: Add stats screen after receiving the pokémon which shows the original gameboy sprite but without the white background. +- UI: In the pokémon list, show the mini menu sprite that you would also see in the party menu in the gameboy games +- UI: add some background images and potentially sprites here and there. +- UI: add some acquisition sound effects from the gameboy games +- UI: add a skippable "trade" 3D animation sequence when you receive a distribution pokémon. The idea is to have a pokéball go into a green mario pipe on either a Nintendo 64 3D model or Nintendo 64 3D logo model. Then follow the pipe with the camera and have the pokéball drop onto a huge 3D representation of the gameboy cartridge before opening the pokéball which then triggers the stats screen. +- UI: Have a 3D intro animation that shows a pokeball opening, a Nintendo 64 logo appearing, slightly jumping and playing the "NINTENDO sixtyfoouuuuuuuuur" meme sound +- UI: Add a menu item to let you buy missing version exclusive pokémon from other versions of the generation with in-game currency. (such as Mankey for Blue or Vulpix for Red). The original games are getting expensive, so this would be a good help for people who can only afford/are willing to buy/play a single game for the generation. +- Support reproduction cartridges (in libpokemegb) +- Support other language versions (in libpokemegb) +- Make it possible to display your cartridge save file as a QR code and contribute to the 3DS' [PKSM](https://github.com/FlagBrew/PKSM) project to migrate the save file easily from gameboy cartridge to 3DS. +- Add Credits screen +- Add a menu item that informs you about the existence of [Poke Transporter GB](https://github.com/GearsProgress/Poke_Transporter_GB) for transferring to Gen3 +- Add some background music (Creative commons remakes/remixes of the original music (Maybe a looped chunk of [Ramstar - Route 24](https://www.youtube.com/watch?v=ih53Nb34vbM)?) +- 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) + +I'm likely going to postpone the 3D stuff (intro and "trade" sequence) until I have implemented a lot of the other ideas here. + +Originally I had the idea of showing a professor character in the rom. But then I found out on YouTube that GearsProgress recently released the "Poke transporter GB" which did the exact same thing. So I'm not sure about that idea anymore. Then again, maybe it could be an option to have some lore that these professors would know eachother. Right now this remains unimplemented though. + +Anyway, how many of these ideas actually get implemented kinda depends on how quickly I get burned out of the project. We'll see how far we get. + +# Help wanted +Hi! I would very much like some people to join the project and add some sprites/graphics design to this rom. I'm not asking you to edit the code for those things. But I don't exactly have the skillset to +effectively design images or create music. + +So if you're good at those things, I would very much appreciate your contributions to bring this project to the next level! + +It goes without saying that I'd also happily accept code contributions. :-) + + diff --git a/assets/Arial.ttf b/assets/Arial.ttf new file mode 100755 index 0000000000000000000000000000000000000000..7ff88f22869126cc992030f18e0eeff65ec8bbac GIT binary patch literal 275572 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 YR_Tt=P6 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

L%Yj0kwC)JKzW*Z{;59WN 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≪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 NC1tZOiJIot{vZ5_jpG0S literal 0 HcmV?d00001 diff --git a/assets/hand-cursor.png b/assets/hand-cursor.png new file mode 100755 index 0000000000000000000000000000000000000000..04f3a7a8170bc64260402492635b2cbe4d182689 GIT binary patch literal 4415 zcmeHLc~BH*7H<^7C4xudu@IUek+{;+Jw12VjDRyEGs55q6TpbUp6(v#WiGlKW`HPJ zNNP8dgy?!if+lXza!Ig~ie@z?g+^1+7$fSM1YIq>B8iE_EnQuYa=&iwN~*S~+Wf~< z_4U5@e($~C_r6!(H!nHzvg2ZA$3PGiXS15~!E-ja;=?C`dsO|Wr@^DI)>$m%lR-GZ z`Q5CCfrXj?12ZAk4MCwZuPj;i>OpwY{j1ZB4W$PHi6@^pd45H6=QPY-@VArCeA@H= ztoS)Ub!KGOw$8DvMV{r3ZCX*-vFPB|uJ?A=&AGNJ9O0h(d|J|)gv`~;vx}enIZ5|b zV&yAy4A)A{MQPiwEZWuk<+Y^JM#mRvOP+E(zL{-`&1##v+H>f*V_n6Kv!^~xJJ(Ti z^dC>6o6hyv4;zbGn_km@w|Db`({*)=J~$QbHsnOK&_#s}MTlwJmZQ7K+t)X~uwZ%$ z)|O|z+Y?@ z)|G3F)`OXepOqHmG&vK0gfAwv#WXB;9$L~~93H!^qpQ8~$hOk1wiH`s<@A!+$*a-} zlS*2vssfLOABayVi%hf3n0uq%w$<`-yv(d=@6f*O}`j-ag#mg>c@){ zNye5kKWz)v+ilc!_`*XP?m)oCT zLxrh#yq1`8C;9&MuhaAFAK5PG5<>6))Vu`V6T55nhYwbI&UQ@O|8Ps`**RrFoD8Fo0o z=%{x8ffTIpuRm5b2mka@-M!nn`#O``uX3N{&)Y+R60K%IDHhvv3Ci!4leFK($V1)$ zs8I+qWP}1FRlx|biz#D$Mp@s!P8rP7Mp=>Cj@bhyrku6ba!f&Oo|CGrp!BpXV}6Vw zL;wIUBam>&>+$hK$S9L=3Ggn8Q5h_$2o*+IvE2ci{2T+T?pN0yGf5#wU;=#K)(K5d9cthNn0- zAh3QPEMk%_f0baA$-p=~7@s#_w-3Sl_yHCGA83dSpb9yLdcEjK3tq5P1CW7$e$|3^ zf)$PCGrYfwqZmsy;}cRxLeSKZeV~f-Na@fN%6J$rQ00MF#VD8AHoIfULZqOK^#&v> zK=vq2fprg)H7Yi7L`r95Ai#VGca-*E?vgUlvfBx>pQ;kWvzd)D(LX`^DV8RrSDg;S zwWL;!s9ahaQIQG-qSI0uM5)nHYFwe9aY{b|%I4z*(nm2O6abgA07s*xbtFxy5FMjc zAu1isAS9_!B6^C(-71n+sr2p<5Q{k$R3+&d8I=e{11LT2)@U?(9fDCVEuzvYH3+HG z04TSMq-mF0rE@7ID4HS``Z+HNrjzxOWeghdl}Q64;Y60hW|ZM_Y*^y(kb)Z+fHlDS zXn&9&9&)l?ra&M?J{4LVQ)qO099Q6Kl~OybRLF2Vs6`P~fytFRX+)eD0;B_|CB;ew z0FoSJLzp;*6#Sgi@AnvGVofoTa$?Nnfd3MVjp9tQ3s z^!+sJuK8cu;_`tF{gZBGdEmcB67{dC0;aP6tN+u(N{b1GrDZ{oRDTLQS#$mJOZvjfl6G#PYq1f+|l~Maz>m7KpBE*kr9;1(1B!8v11HoYe0X&he1*p(PDsI zzYMf4&ZcHBQ%<7#NfCcy;~X z=!zLWOff#N0}6tp(wXpz_rNi0sw3BFkq%r?{FJynhkf0Kzx?~ot-a}uERco?`FRCU z=6BzMzXqT%S5CGW`g25*6?{YotV?+aik&82VNgr!OdyODY<5dzZ)8N&?&)ga_Uhj4+C9^ich9;m{`{E~ Uv^n>r4)}*`mOS&$1uGi<3-&fQG5`Po literal 0 HcmV?d00001 diff --git a/assets/menu-bg-9slice.png b/assets/menu-bg-9slice.png new file mode 100755 index 0000000000000000000000000000000000000000..374629cac3859963b82976138d09fcfb5779f54e GIT binary patch literal 359 zcmeAS@N?(olHy`uVBq!ia0vp@Ak4uAB#T}@sR2@KN#5=*4F5rJ!QSPQfg+p*9+AZi z419+{nDKc2iWHzAdx@v7EBgy}7EVL$yp4NHfkIzAT^vI+&i76-^kZ_AXtiH{LuZ0O z)=q|?fXu|?Bn{nb3*-)*k`|bht9#){Z#CaK2OkbMsjLSqmu~Iw&Y5@gO&|9p-t{y8 z@BO*|OvzCf3Z4%j`{X<2>le(0)sUpEWf z@@3!qO#V;N0~eRwi?2*JnL6d2$+F10$>0Am&B%SRzu_qJ0igdGJYD@<);T3K0RS9O BmH+?% literal 0 HcmV?d00001 diff --git a/assets/oak.png b/assets/oak.png new file mode 100755 index 0000000000000000000000000000000000000000..bc08c45b776d3431c8581debf08157aa7fb84689 GIT binary patch literal 5508 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 +#include + +class IAnimation; +typedef std::vector IAnimationList; + +/** + * @brief This class keeps track of animations and is used to apply an animation step on all pending animations + * on every frame + */ +class AnimationManager +{ +public: + void add(IAnimation* animation); + void remove(IAnimation* animation); + + void step(uint32_t elapsedTimeInMs); +protected: +private: + IAnimationList animations_; +}; + +#endif \ No newline at end of file diff --git a/include/animations/IAnimation.h b/include/animations/IAnimation.h new file mode 100755 index 0000000..2d67296 --- /dev/null +++ b/include/animations/IAnimation.h @@ -0,0 +1,144 @@ +#ifndef _IANIMATION_H +#define _IANIMATION_H + +#include + +/** + * A Distance time function indicates at a certain normalized time point in the [0.0, 1.0] range in the animation, + * how much of the total movement the animation should have done (also normalized in the [0.0, 1.0]) + * + * This gives you a way to control the acceleration/deceleration of the animation. + */ +enum class AnimationDistanceTimeFunctionType +{ + /** + * No distance time function means no animation -> The animation will immediately be move to the end position (1.0f) + */ + NONE, + + /** + * Linear distance time function means that the animation always moves at the same speed + */ + LINEAR, + + /** + * The Ease-in Ease-out distance time function slowly accelerates the animation at the start and slowly decelerates the animation at the end + */ + EASE_IN_EASE_OUT +}; + +/** + * @brief This enum defines what loop type the animation has (if any) + */ +enum class AnimationLoopType +{ + /** + * No loop + */ + NONE, + + /** + * After the end of the animation is reached, it restarts from the beginning + */ + NORMAL_LOOP, + + /** + * After the end of the animation is reached, it will start going backwards until the beginning is reached again. And then it will go forward again. + * This cycle repeats until someone/something stops the animation. + */ + BACK_AND_FORTH +}; + +class IAnimation +{ +public: + virtual ~IAnimation(); + + virtual AnimationDistanceTimeFunctionType getDistanceTimeFunctionType() const = 0; + + /** + * Applies the specified step. This should be a stepSize in the [0.f - 1.f] range + */ + virtual void step(float stepSize, bool suppressFinishedCallback = false) = 0; + + /** + * @brief returns the duration of the animation in milliseconds + */ + virtual uint32_t getDurationInMs() const = 0; + + /** + * Skips to the end of the animation + */ + virtual void skipToEnd() = 0; + + /** Indicates whether this animation is finished and should therefore be removed from AnimationManager */ + virtual bool isFinished() const = 0; + + /** + * Returns the current loop type (if any) + */ + virtual AnimationLoopType getLoopType() const = 0; + + /** + * Sets the loop type (if any) + */ + virtual void setLoopType(AnimationLoopType loopType) = 0; + + /** + * @brief Sets a callback that should be called when the animation has finished. + * Note: this only applies to scenarios where AnimationLoopType == NONE + */ + virtual void setAnimationFinishedCallback(void* context, void (*animationFinishedCb)(void*)) = 0; +protected: + /** + * @brief This function applies the relative pos [0.f-1.f] to the + * specific animation implementation. + * + * This pos indicates the point in the animation between the start and the end, not in function of time + * but in function of distance (the actual point that needs to be applied) + */ + virtual void apply(float pos) = 0; +private: +}; + +/** + * @brief Abstract implementation of common functionality defined in IAnimation + * + */ +class AbstractAnimation : public IAnimation +{ +public: + AbstractAnimation(float initialTimePos = 0); + virtual ~AbstractAnimation(); + + void step(float stepSize, bool suppressFinishedCallback = false) override; + + void skipToEnd() override; + bool isFinished() const override; + + /** + * Returns the current loop type (if any) + */ + AnimationLoopType getLoopType() const override; + + /** + * Sets the loop type (if any) + */ + void setLoopType(AnimationLoopType loopType) override; + + /** + * @brief Sets a callback that should be called when the animation has finished. + * Note: this only applies to scenarios where AnimationLoopType == NONE + */ + void setAnimationFinishedCallback(void* context, void (*animationFinishedCb)(void*)); + +protected: + float currentTimePos_; +private: + void (*animationFinishedCb_)(void*); + void *animationFinishedCallbackContext_; + AnimationLoopType loopType_; + bool isStepIncreasing_; +}; + +#endif \ No newline at end of file diff --git a/include/animations/MoveAnimation.h b/include/animations/MoveAnimation.h new file mode 100755 index 0000000..4ac84b6 --- /dev/null +++ b/include/animations/MoveAnimation.h @@ -0,0 +1,37 @@ +#ifndef _MOVEANIMATION_H +#define _MOVEANIMATION_H + +#include "animations/IAnimation.h" +#include "core/common.h" + +class IWidget; + +/** + * This animation manipulates the bounds of a Widget over time + */ +class MoveAnimation : public AbstractAnimation +{ +public: + MoveAnimation(IWidget* target); + virtual ~MoveAnimation(); + + AnimationDistanceTimeFunctionType getDistanceTimeFunctionType() const override; + uint32_t getDurationInMs() const override; + + void start(const Rectangle& startBounds, const Rectangle& moveVectorStartEnd, uint32_t durationInMs); + + /** * + * reset the animation back to its start position + */ + void reset(); +protected: +private: + void apply(float pos) override; + + IWidget* target_; + Rectangle startBounds_; + Rectangle diffStartEnd_; + uint32_t durationInMs_; +}; + +#endif \ No newline at end of file diff --git a/include/core/Application.h b/include/core/Application.h new file mode 100755 index 0000000..8fa3202 --- /dev/null +++ b/include/core/Application.h @@ -0,0 +1,29 @@ +#ifndef _APPLICATION_H +#define _APPLICATION_H + +#include "animations/AnimationManager.h" +#include "scenes/SceneManager.h" +#include "core/RDPQGraphics.h" +#include "core/FontManager.h" +#include "transferpak/TransferPakManager.h" + +class Application +{ +public: + Application(); + ~Application(); + + void init(); + + void run(); +protected: +private: + RDPQGraphics graphics_; + AnimationManager animationManager_; + FontManager fontManager_; + TransferPakManager tpakManager_; + SceneManager sceneManager_; + Rectangle sceneBounds_; +}; + +#endif \ No newline at end of file diff --git a/include/core/DragonUtils.h b/include/core/DragonUtils.h new file mode 100755 index 0000000..0b87ef0 --- /dev/null +++ b/include/core/DragonUtils.h @@ -0,0 +1,29 @@ +#ifndef _DRAGONUTILS_H +#define _DRAGONUTILS_H + +#include + +enum class UINavigationKey +{ + NONE, + UP, + RIGHT, + DOWN, + LEFT +}; + +enum class NavigationInputSourceType +{ + NONE, + ANALOG_STICK, + DPAD, + BOTH +}; + +/** + * This function determines whether the joypad_inputs_t has analog or dpad positions/presses that could be considered for UI navigation. + * If so, it will return the most prominent direction. + */ +const UINavigationKey determineUINavigationKey(joypad_inputs_t inputs, NavigationInputSourceType sourceType); + +#endif \ No newline at end of file diff --git a/include/core/FontManager.h b/include/core/FontManager.h new file mode 100755 index 0000000..f650555 --- /dev/null +++ b/include/core/FontManager.h @@ -0,0 +1,49 @@ +#ifndef _FONTMANAGER_H +#define _FONTMANAGER_H + +#include +#include +#include + +typedef struct FontEntry +{ + rdpq_font_t* font; + uint8_t fontId; +} FontEntry; + +typedef std::unordered_map RDPQFontMap; + +/** + * @brief This class exists because libdragon does not offer a way to unload a font or otherwise load a font + * more than once. + * . + * Therefore if you want to load the font again in a different scene, you hit an assert. + * + * FontManager prevents this by handling the loading transparently and returning already loaded font handles + * if the desired font was already loaded before. + * + */ +class FontManager +{ +public: + FontManager(); + + /** + * Retrieve a fontId for the font at the given URI + */ + uint8_t getFont(const char* fontUri); + + /** + * This function registers the given fontStyle onto the given font and associate it with the specified fontStyleId + * Note: there's no unregisterFontStyle because libdragon doesn't offer the functionality. + * + * That being said, replacing the fontStyleId is possible without it throwing an assert() in our face + */ + void registerFontStyle(uint8_t fontId, uint8_t fontStyleId, const rdpq_fontstyle_t& fontStyle); +protected: +private: + RDPQFontMap fontMap_; + uint8_t nextFontId_; +}; + +#endif \ No newline at end of file diff --git a/include/core/RDPQGraphics.h b/include/core/RDPQGraphics.h new file mode 100755 index 0000000..0859e16 --- /dev/null +++ b/include/core/RDPQGraphics.h @@ -0,0 +1,77 @@ +#ifndef _RDPQGRAPHICS_H +#define _RDPQGRAPHICS_H + +#include "core/common.h" + +#include +#include + +typedef struct TextRenderSettings +{ + uint8_t fontId; + uint8_t fontStyleId; + int16_t charSpacing; ///< Extra spacing between chars (in addition to glyph width and kerning) + int16_t lineSpacing; ///< Extra spacing between lines (in addition to font height) +} TextRenderSettings; + +typedef struct SpriteRenderSettings SpriteRenderSettings; + +/** + * @brief This class abstracts operations done with the RDPQ graphics API in libdragon + * It probably wasn't necessary, but it makes me feel better :) + */ +class RDPQGraphics +{ +public: + RDPQGraphics(); + ~RDPQGraphics(); + + void init(); + void destroy(); + + void triggerDebugFrame(); + + /** + * @brief This function marks the start of a new frame. + * In practice, it attaches the RDP to a new/different framebuffer and clears it + */ + void beginFrame(); + + /** + * @brief This function marks the end of a frame + * In practice, it detaches the RDP from a framebuffer and submits it for showing to the user. + * (when it's done rendering) + */ + void finishAndShowFrame(); + + /** + * @brief This function renders a filled rectangle with the specified color + * at the specified absolute destination rectangle + */ + void fillRectangle(const Rectangle& dstRect, color_t color); + + /** + * This function can be used to draw text on screen with the specified fontId at the given destionation rectangle. + * + * The thing is: there's no way to influence the font size: this is determined at compilation when you create the .font64 file in the Makefile. + * So we need different font instances for different font sizes and all need to have been generated at compile time. + */ + void drawText(const Rectangle& dstRect, const char* text, const TextRenderSettings& renderSettings); + + /** + * @brief This function can be used to draw an image/sprite at the specified destination + * with the specified SpriteRenderSettings. Please refer to SpriteRenderSettings for more info. + */ + void drawSprite(const Rectangle& dstRect, sprite_t* sprite, const SpriteRenderSettings& renderSettings); + + const Rectangle& getClippingRectangle() const; + void setClippingRectangle(const Rectangle& clipRect); + void resetClippingRectangle(); +protected: +private: + Rectangle clipRect_; + bool initialized_; + bool debugFrameTriggered_; +}; + +#endif \ No newline at end of file diff --git a/include/core/Sprite.h b/include/core/Sprite.h new file mode 100755 index 0000000..24fc219 --- /dev/null +++ b/include/core/Sprite.h @@ -0,0 +1,63 @@ +#ifndef _SPRITE_H +#define _SPRITE_H + +#include "core/common.h" + +typedef struct sprite_s sprite_t; + + +/** + * The render mode of the sprite. We have NORMAL and NINESLICE + * NINESLICE is used to render and stretch a 9sliced image similar to 9slice images + * are/were used on the web. (for things like rounded corners or otherwise custom boxes) + */ +enum class SpriteRenderMode +{ + NORMAL = 0, + NINESLICE +}; + +typedef struct SpriteRenderSettings +{ + /* + * Indicates the render mode of the sprite + * + * If you use NINESLICE, you MUST specify a 9 slice rectangle in the srcRect field. + * Please refer to the comments there for more info. + */ + SpriteRenderMode renderMode; + + /** + * Either a source region within the sprite that needs to be rendered if the SpriteRenderMode is set to NORMAL + * or a 9slice rectangle if the SpriteRenderMode is set to NINESLICE + * + * A 9slice rectangle is special because it is usually used to specify a box-like image with + * special corners/edges using an extremely small image. + * + * One typical use case for this is when you want to draw rounded corners. + * + * To specify a 9slice image, the x, y, width and height properties of srcRect have a different meaning: + * x -> the width of each corner on the left size of the image + * y -> the height of each corner on the left size of the image + * width -> the width of each corner on the right size of the image + * height -> the height of each corner on the right size of the image + * + * Our RDPQGraphics class will use these values to stretch a 9slice image according to the + * destination rectangle specified in RDPQGraphics::drawSprite(). + * The corners will never get stretched, but the edges and middle portion will + * + * Note: because of the properties of a 9slice image, the image doesn't really need to be any bigger + * than the widths and heights of all corners combined + 1 pixel in each direction. + * + * For example: if my corners are all 6x6 pixels, then it suffices to create an image that is 13x13 pixels + * to render the box element. + */ + Rectangle srcRect; + + /** + * @brief Rotation angle in radians + */ + float rotationAngle; +} SpriteRenderSettings; + +#endif \ No newline at end of file diff --git a/include/core/common.h b/include/core/common.h new file mode 100755 index 0000000..6e3be83 --- /dev/null +++ b/include/core/common.h @@ -0,0 +1,35 @@ +#ifndef _CORE_COMMON_H +#define _CORE_COMMON_H + +typedef struct Dimensions +{ + int width; + int height; +} Dimensions; + +typedef struct Rectangle +{ + int x; + int y; + int width; + int height; +} Rectangle; + +/** + * Whether or not the rectangle has a size of 0. + */ +bool isZeroSizeRectangle(const Rectangle& rect); + +/** + * @brief This function adds the x,y coordinates of rectangle b + * to rectangle a and returns a new rectangle with these combined x,y coords and + * the width and height of rectangle a + */ +Rectangle addOffset(const Rectangle& a, const Rectangle& b); + +/** + * @brief Extract a Dimensions struct from a Rectangle that only contains the width and height + */ +Dimensions getDimensions(const Rectangle& r); + +#endif \ No newline at end of file diff --git a/include/menu/MenuEntries.h b/include/menu/MenuEntries.h new file mode 100755 index 0000000..a4818bd --- /dev/null +++ b/include/menu/MenuEntries.h @@ -0,0 +1,15 @@ +#ifndef _MENUENTRIES_H +#define _MENUENTRIES_H + +#include "widget/MenuItemWidget.h" + +extern MenuItemData gen1MenuEntries[]; +extern const uint32_t gen1MenuEntriesSize; + +extern MenuItemData gen2MenuEntries[]; +extern const uint32_t gen2MenuEntriesSize; + +extern MenuItemData gen2CrystalMenuEntries[]; +extern const uint32_t gen2CrystalMenuEntriesSize; + +#endif \ No newline at end of file diff --git a/include/menu/MenuFunctions.h b/include/menu/MenuFunctions.h new file mode 100755 index 0000000..72d3eff --- /dev/null +++ b/include/menu/MenuFunctions.h @@ -0,0 +1,15 @@ +#ifndef _MENUFUNCTIONS_H +#define _MENUFUNCTIONS_H + +void printMessage(void* context, const void* param); +void activateFrameLog(void* context, const void* param); + +void goToTestScene(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 gen2ReceiveGSBall(void* context, const void* param); + +#endif \ No newline at end of file diff --git a/include/scenes/AbstractUIScene.h b/include/scenes/AbstractUIScene.h new file mode 100755 index 0000000..658023d --- /dev/null +++ b/include/scenes/AbstractUIScene.h @@ -0,0 +1,83 @@ +#ifndef _ABSTRACTUISCENE_H +#define _ABSTRACTUISCENE_H + +#include "scenes/IScene.h" +#include + +class IWidget; + +/** + * The WidgetFocusChain is an important event handling mechanism in AbstractUIScene based scenes. + * + * If the current widget does not handle a navigation key input, it will determine to which widget the focus should be shifted if any + * + * It's so integral into the AbstractUIScene based scenes, that there's no input handling without setting one. So don't forget to set one! + */ +typedef struct WidgetFocusChainSegment +{ + /** + * @brief widget that is supposed to be focused when this segment is active + */ + IWidget* current; + /** + * @brief WidgetFocusChainSegment that is supposed to become active when the user navigates to the left with analog stick or dpad + */ + WidgetFocusChainSegment* onLeft; + /** + * @brief WidgetFocusChainSegment that is supposed to become active when the user navigates to the right with analog stick or dpad + */ + WidgetFocusChainSegment* onRight; + /** + * @brief WidgetFocusChainSegment that is supposed to become active when the user navigates upwards with analog stick or dpad + */ + WidgetFocusChainSegment* onUp; + + /** + * @brief WidgetFocusChainSegment that is supposed to become active when the user navigates downwards with analog stick or dpad + */ + WidgetFocusChainSegment* onDown; +} WidgetFocusChainSegment; + +/** + * @brief This abstract IScene implementation implements some common functionality for UI scenes. + * + * The common functionality here is: + * - user input handling and forwarding. + * - focus handling -> if the focused widget hasn't handled a directional input on analog stick/dpad, AbstractUIScene will check the focusChain if + * the focus needs to be switched to a different widget. + */ +class AbstractUIScene : public IScene +{ +public: + AbstractUIScene(SceneDependencies& deps); + virtual ~AbstractUIScene(); + + /** + * @brief This function reads the user input and forwards it to the focused widget (as determined by the focusChain) + * if the focused widget doesn't handle any pressed navigational keys, AbstractUIScene will check the current WidgetFocusChainSegment + * to see if a different widget can be selected for the given direction. + */ + void processUserInput() override; + bool handleUserInput(joypad_port_t port, const joypad_inputs_t& inputs) override; + + void destroy() override; + + /** + * This function sets a focus chain onto the scene. + * + * The focus chain is an important part of the event handling chain in the AbstractUIScene based screens + * + * The initial focus is set by this and events are basically being directed according to the focusChain. + * Without a focusChain, there's no event handling. At least for AbstractUIScene based Scenes + * + * So don't forget to set one! + */ + void setFocusChain(WidgetFocusChainSegment* focusChain); +protected: + SceneDependencies& deps_; +private: + WidgetFocusChainSegment* focusChain_; + uint64_t lastInputHandleTime_; +}; + +#endif \ No newline at end of file diff --git a/include/scenes/DistributionPokemonListScene.h b/include/scenes/DistributionPokemonListScene.h new file mode 100755 index 0000000..603ac4a --- /dev/null +++ b/include/scenes/DistributionPokemonListScene.h @@ -0,0 +1,83 @@ +#ifndef _DISTRIBUTIONPOKEMONLISTSCENE_H +#define _DISTRIBUTIONPOKEMONLISTSCENE_H + +#include "scenes/MenuScene.h" +#include "transferpak/TransferPakRomReader.h" +#include "transferpak/TransferPakSaveManager.h" +#include "gen1/Gen1GameReader.h" +#include "gen2/Gen2GameReader.h" + +/** + * @brief The Distribution Pokémon list we want to show + */ +enum class DistributionPokemonListType +{ + INVALID, + /** + * @brief Main Gen 1 Distribution events + */ + GEN1, + /** + * @brief Main Gen 2 Distribution events + */ + GEN2, + /** + * @brief Gen 2 Pokémon Center New York distribution pokémon + * + */ + GEN2_POKEMON_CENTER_NEW_YORK +}; + +struct DistributionPokemonListSceneContext : public MenuSceneContext +{ + DistributionPokemonListType listType; +}; + +typedef DistributionPokemonListSceneContext DistributionPokemonListSceneContext; + +/** + * @brief This scene implementation gives you a list of pokémon to choose and allow you to inject them into your cartridge save by selecting one + * and pressing the A-button + */ +class DistributionPokemonListScene : public MenuScene +{ +public: + DistributionPokemonListScene(SceneDependencies& deps, void* context); + virtual ~DistributionPokemonListScene(); + + void init() override; + void destroy() override; + + bool handleUserInput(joypad_port_t port, const joypad_inputs_t& inputs) override; + + /** + * This function will start the pokémon injection. and show a non-skippable "saving" dialog + * The actual injection will be done on the next handleUserInput() call. + * That will ensure that at least 1 render() call has been handled before we start doing the work + */ + void triggerPokemonInjection(const void* data); + + /** + * @brief The core functionality of this class: it will inject the selected pokémon into your cartridge save. + * + * @param data a pointer to the Gen1DistributionPokemon or Gen2DistributionPokemon instance you want to inject. + */ + void injectPokemon(const void* data); + + void onDialogDone() override; +protected: + void setupMenu() override; +private: + void loadDistributionPokemonList(); + + TransferPakRomReader romReader_; + TransferPakSaveManager saveManager_; + Gen1GameReader gen1Reader_; + Gen2GameReader gen2Reader_; + DialogData diag_; + const void* pokeToInject_; +}; + +void deleteDistributionPokemonListSceneContext(void* context); + +#endif \ No newline at end of file diff --git a/include/scenes/IScene.h b/include/scenes/IScene.h new file mode 100755 index 0000000..2d387e7 --- /dev/null +++ b/include/scenes/IScene.h @@ -0,0 +1,55 @@ +#ifndef _ISCENE_H +#define _ISCENE_H + +#include + +class RDPQGraphics; +class SceneManager; +class AnimationManager; +class FontManager; +class TransferPakManager; + +typedef struct Rectangle Rectangle; + +enum class SceneType +{ + NONE, + INIT_TRANSFERPAK, + MENU, + DISTRIBUTION_POKEMON_LIST, + TEST +}; + +typedef struct SceneDependencies +{ + RDPQGraphics& gfx; + AnimationManager& animationManager; + FontManager& fontManager; + TransferPakManager& tpakManager; + SceneManager& sceneManager; + uint8_t generation; + uint8_t specificGenVersion; +} SceneDependencies; + +class IScene +{ +public: + virtual ~IScene(); + + virtual void init() = 0; + virtual void destroy() = 0; + + /** + * @brief This function should implement the procedure of obtaining the relevant user input + * and directing it as necessary, but it should not implement how to handle it. For the latter you should implement handleUserInput instead. + */ + virtual void processUserInput() = 0; + + virtual bool handleUserInput(joypad_port_t port, const joypad_inputs_t& inputs) = 0; + + virtual void render(RDPQGraphics& gfx, const Rectangle& sceneBounds) = 0; +protected: +private: +}; + +#endif \ No newline at end of file diff --git a/include/scenes/InitTransferPakScene.h b/include/scenes/InitTransferPakScene.h new file mode 100755 index 0000000..509af6e --- /dev/null +++ b/include/scenes/InitTransferPakScene.h @@ -0,0 +1,44 @@ +#ifndef _INITTRANSFERPAKSCENE_H +#define _INITTRANSFERPAKSCENE_H + +#include "scenes/SceneWithDialogWidget.h" +#include "widget/TransferPakDetectionWidget.h" + +#define PLAYER_NAME_SIZE 15 + +class TransferPakManager; + +/** + * @brief In this scene implementation, we do the detection of the N64 transfer pak and whether a supported Pokémon game was + * detected. + */ +class InitTransferPakScene : public SceneWithDialogWidget +{ +public: + InitTransferPakScene(SceneDependencies& deps, void* context); + virtual ~InitTransferPakScene(); + + void init() override; + void destroy() override; + + void render(RDPQGraphics& gfx, const Rectangle& sceneBounds) override; + + void onDialogDone(); + void onTransferPakWidgetStateChanged(TransferPakWidgetState newState); +protected: +private: + void setupTPakDetectWidget(); + void setupDialog(DialogWidgetStyle& style) override; + + void loadGameMetadata(); + const char* getGameTypeString(); + + sprite_t* menu9SliceSprite_; + TransferPakDetectionWidget tpakDetectWidget_; + WidgetFocusChainSegment tpakDetectWidgetSegment_; + DialogData diagData_; + char playerName_[PLAYER_NAME_SIZE]; + const char* gameTypeString_; +}; + +#endif \ No newline at end of file diff --git a/include/scenes/MenuScene.h b/include/scenes/MenuScene.h new file mode 100755 index 0000000..9de98e7 --- /dev/null +++ b/include/scenes/MenuScene.h @@ -0,0 +1,68 @@ +#ifndef _MAINMENUSCENE_H +#define _MAINMENUSCENE_H + +#include "scenes/SceneWithDialogWidget.h" +#include "widget/VerticalList.h" +#include "widget/DialogWidget.h" +#include "widget/CursorWidget.h" +#include "widget/MenuItemWidget.h" +#include "widget/ListItemFiller.h" +#include "widget/IFocusListener.h" + +typedef struct MenuSceneContext +{ + MenuItemData* menuEntries; + uint32_t numMenuEntries; +} MenuSceneContext; + +/** + * @brief A scene showing a menu + * + */ +class MenuScene : public SceneWithDialogWidget, public IFocusListener +{ +public: + MenuScene(SceneDependencies& deps, void* context); + virtual ~MenuScene(); + + void init() override; + void destroy() override; + + void render(RDPQGraphics& gfx, const Rectangle& sceneBounds) override; + + bool handleUserInput(joypad_port_t port, const joypad_inputs_t& inputs) override; + + virtual void onDialogDone(); + + void focusChanged(const FocusChangeStatus& status) override; + + SceneDependencies& getDependencies(); + + /** + * This is a helper function to show a single message to the user. + * It should only be used for simple situations. (feedback on a function executed directly in the menu for example) + */ + void showSingleMessage(const DialogData& messageData); +protected: + virtual void setupMenu(); + void setupFonts() override; + void setupDialog(DialogWidgetStyle& style) override; + + virtual void showDialog(DialogData* diagData); + + MenuSceneContext* context_; + sprite_t* menu9SliceSprite_; + sprite_t* cursorSprite_; + VerticalList menuList_; + CursorWidget cursorWidget_; + ListItemFiller menuListFiller_; + WidgetFocusChainSegment listFocusChainSegment_; + uint8_t fontStyleYellowId_; + bool bButtonPressed_; +private: + DialogData singleMessageDialog_; +}; + +void deleteMenuSceneContext(void* context); + +#endif \ No newline at end of file diff --git a/include/scenes/SceneManager.h b/include/scenes/SceneManager.h new file mode 100755 index 0000000..632a7b6 --- /dev/null +++ b/include/scenes/SceneManager.h @@ -0,0 +1,72 @@ +#ifndef _SCENEMANAGER_H +#define _SCENEMANAGER_H + +#include "scenes/IScene.h" +#include + +class RDPQGraphics; +class AnimationManager; +class FontManager; +class TransferPakManager; + +typedef struct Rectangle Rectangle; + +enum class SceneType; + +typedef struct SceneHistorySegment +{ + SceneType type; + void* context; + void (*deleteContextFunc)(void*); +} SceneHistorySegment; + +/** + * @brief The SceneManager handles switching between IScene objects. (loading, unloading, forwarding input and render requests) + * + */ +class SceneManager +{ +public: + SceneManager(RDPQGraphics& gfx, AnimationManager& animationManager, FontManager& fontManager, TransferPakManager& tpakManager); + ~SceneManager(); + + /** + * This function stores the given scenetype to be loaded + * on the next render() call. + * + * The reason for this deferred loading is that you're usually triggering the switchScene call from within the current Scene. + * If not implemented this way, the current Scene object would be free'd while a member function would still be running on it (use-after-free => kaboom) + * So, by deferring the scene switch, we make sure the current Scene instance is done executing whatever before we swipe away the carpet from beneath its feet. + * + * WARNING: If you specify a sceneContext, you MUST also specify a deleteContextFunc callback function pointer. + * This is needed because you can't call delete() on a void*. And we need to keep the context around in the sceneHistory for as long as it needs to + */ + void switchScene(SceneType sceneType, void (*deleteContextFunc)(void*) = nullptr, void* sceneContext = nullptr); + + /** + * @brief Switch back to the previous scene in the history stack + */ + void goBackToPreviousScene(); + + /** + * @brief Clears the history stack + */ + void clearHistory(); + + void handleUserInput(); + void render(const Rectangle& sceneBounds); +protected: +private: + void loadScene(); + void unloadScene(IScene* scene); + + std::vector sceneHistory_; + SceneDependencies sceneDeps_; + IScene* scene_; + SceneType newSceneType_; + void* newSceneContext_; + void* contextToDelete_; + void (*deleteContextFunc_)(void*); +}; + +#endif \ No newline at end of file diff --git a/include/scenes/SceneWithDialogWidget.h b/include/scenes/SceneWithDialogWidget.h new file mode 100755 index 0000000..f80ab7b --- /dev/null +++ b/include/scenes/SceneWithDialogWidget.h @@ -0,0 +1,28 @@ +#ifndef _SCENE_WITH_DIALOG_WIDGET_H +#define _SCENE_WITH_DIALOG_WIDGET_H + +#include "scenes/AbstractUIScene.h" +#include "widget/DialogWidget.h" + +class SceneWithDialogWidget : public AbstractUIScene +{ +public: + SceneWithDialogWidget(SceneDependencies& deps); + virtual ~SceneWithDialogWidget(); + + void init() override; + void destroy() override; + + void render(RDPQGraphics& gfx, const Rectangle& sceneBounds) override; +protected: + virtual void setupFonts(); + virtual void setupDialog(DialogWidgetStyle& style); + + DialogWidget dialogWidget_; + WidgetFocusChainSegment dialogFocusChainSegment_; + uint8_t arialId_; + uint8_t fontStyleWhiteId_; +private: +}; + +#endif \ No newline at end of file diff --git a/include/scenes/TestScene.h b/include/scenes/TestScene.h new file mode 100755 index 0000000..b882356 --- /dev/null +++ b/include/scenes/TestScene.h @@ -0,0 +1,36 @@ +#ifndef _TESTSCENE_H +#define _TESTSCENE_H + +#include "scenes/AbstractUIScene.h" +#include "core/RDPQGraphics.h" +#include "core/Sprite.h" + +class TestScene : public AbstractUIScene +{ +public: + TestScene(SceneDependencies& deps, void* sceneContext); + virtual ~TestScene(); + + void init() override; + void destroy() override; + + void render(RDPQGraphics& gfx, const Rectangle& sceneBounds) override; +protected: +private: + rdpq_font_t* arialFont_; + uint8_t arialFontId_; + uint8_t fontStyleWhite_; + sprite_t* pokeballSprite_; + sprite_t* oakSprite_; + sprite_t* menu9SliceSprite_; + const Rectangle rectBounds_; + const Rectangle textRect_; + const Rectangle spriteBounds_; + Rectangle oakBounds_; + Rectangle oakSrcBounds_; + const Rectangle menuBounds_; + TextRenderSettings textRenderSettings_; + const SpriteRenderSettings menuRenderSettings_; +}; + +#endif \ No newline at end of file diff --git a/include/transferpak/TransferPakManager.h b/include/transferpak/TransferPakManager.h new file mode 100755 index 0000000..686736c --- /dev/null +++ b/include/transferpak/TransferPakManager.h @@ -0,0 +1,103 @@ +#ifndef _TRANSFERPAKMANAGER_H +#define _TRANSFERPAKMANAGER_H + +#include + +#ifdef __GNUC__ +#define likely(x) __builtin_expect(!!(x), 1) +#define unlikely(x) __builtin_expect(!!(x), 0) +#else +#define likely(x) (x) +#define unlikely(x) (x) +#endif + +enum class TransferPakMode +{ + ROM, + RAM +}; + +/** @brief Transfer Pak command block size (32 bytes) */ +#define TPAK_BLOCK_SIZE 0x20 + +/** + * @brief This class manages the N64 transfer pak + * Both SRAM and ROM access are implemented in the same class here + * because the transfer pak itself has some shenanigans: + * - it has its own banking mechanism independent of the cartridge banks -> It can only access 16 KB of the gameboy address space at a time. However, libdragon + * takes care of most of the heavy lifting. + * - It can't do single byte read/writes. Everything must be done in multiples of 32 bytes. + * - switching transfer pak banks influences whether you are reading/writing the right addresses for ROM or SRAM. + * + * So you can't implement ROM and SRAM reading in separate classes. + * TransferPakManager manages and keeps track of all this and abstracts this complexity. + */ +class TransferPakManager +{ +public: + TransferPakManager(); + ~TransferPakManager(); + + joypad_port_t getPort() const; + void setPort(joypad_port_t port); + + bool hasTransferPak(); + bool setPower(bool on); + uint8_t getStatus(); + + bool validateGbHeader(); + + /** + * @brief This function switches the Gameboy ROM bank index + * WARNING: it switches to transfer pak bank 0 + */ + void switchGBROMBank(uint8_t bankIndex); + + /** + * @brief This function enables/disables gameboy RAM/RTC access. + * WARNING: it switches to transfer pak bank 0 + */ + void setRAMEnabled(bool enabled); + + /** + * @brief This function switches the Gameboy RAM bank index + * WARNING: it switches to transfer pak bank 1 + */ + void switchGBSRAMBank(uint8_t bankIndex); + + /** + * @brief This function reads data from the specified gameboy address + */ + void read(uint16_t gbAddress, uint8_t* data, uint16_t size); + + /** + * @brief This function reads data from the specified SRAM bank offset + */ + void readSRAM(uint16_t SRAMBankOffset, uint8_t *data, uint16_t size); + + /** + * @brief This function writes the given data to the given SRAMBankOffset + * The write is cached and won't be written until a new write outside the current 32 byte block + * is triggered or finishWrites() is called by the user. + * + * WARNING: Don't forget to call finishWrites() when you're done writing! Otherwise corruption will occur + * due to the cached writes + */ + void writeSRAM(uint16_t SRAMBankOffset, const uint8_t *data, uint16_t size); + + /** + * @brief This function writes the current writeBuffer immediately + */ + void finishWrites(); +protected: +private: + joypad_port_t port_; + bool wasPoweredAtLeastOnce_; + uint8_t currentSRAMBank_; + uint16_t readBufferBankOffset_; + uint16_t writeBufferSRAMBankOffset_; + uint8_t readBuffer_[TPAK_BLOCK_SIZE]; + uint8_t writeBuffer_[TPAK_BLOCK_SIZE]; +}; + +#endif \ No newline at end of file diff --git a/include/transferpak/TransferPakRomReader.h b/include/transferpak/TransferPakRomReader.h new file mode 100755 index 0000000..28d036c --- /dev/null +++ b/include/transferpak/TransferPakRomReader.h @@ -0,0 +1,64 @@ +#ifndef _TRANSFERPAKROMREADER_H +#define _TRANSFERPAKROMREADER_H + +#include "RomReader.h" + +class TransferPakManager; + +class TransferPakRomReader : public BaseRomReader +{ +public: + TransferPakRomReader(TransferPakManager& pakManager); + virtual ~TransferPakRomReader(); + + /** + * @brief This function reads a byte, returns it and advances the internal pointer by 1 byte + * + * @return uint8_t + */ + bool readByte(uint8_t& outByte) override; + + /** + * @brief This function reads multiple bytes into the specified output buffer + * + * @return whether the operation was successful + */ + bool read(uint8_t* outBuffer, uint32_t bytesToRead) override; + + /** + * @brief This function reads the current byte without advancing the internal pointer by 1 byte + * + * @return uint8_t + */ + uint8_t peek() override; + + /** + * @brief This function advances the internal pointer by 1 byte + * + */ + bool advance(uint32_t numBytes = 1) override; + + /** + * @brief This function seeks to the specified absolute rom offset + * + * @param absoluteOffset + */ + bool seek(uint32_t absoluteOffset) override; + + /** + * @brief This function searches for a sequence of bytes (the needle) in the buffer starting from + * the current internal position + + * @return true we found the sequence of bytes and we've seeked toward this point + * @return false we didn't find the sequence of bytes anywhere + */ + bool searchFor(const uint8_t* needle, uint32_t needleLength) override; + + uint8_t getCurrentBankIndex() const override; +protected: +private: + TransferPakManager& pakManager_; + uint32_t currentRomOffset_; +}; + +#endif \ No newline at end of file diff --git a/include/transferpak/TransferPakSaveManager.h b/include/transferpak/TransferPakSaveManager.h new file mode 100755 index 0000000..35787a1 --- /dev/null +++ b/include/transferpak/TransferPakSaveManager.h @@ -0,0 +1,62 @@ +#ifndef _TRANSFERPAKSAVEMANAGER_H +#define _TRANSFERPAKSAVEMANAGER_H + +#include "SaveManager.h" + +class TransferPakManager; + +class TransferPakSaveManager : public BaseSaveManager +{ +public: + TransferPakSaveManager(TransferPakManager& pakManager); + virtual ~TransferPakSaveManager(); + + /** + * @brief This function reads a byte, returns it and advances the internal pointer by 1 byte + * + * @return uint8_t + */ + bool readByte(uint8_t& outByte) override; + void writeByte(uint8_t byte) override; + + /** + * @brief This function reads multiple bytes into the specified output buffer + * + * @return whether the operation was successful + */ + bool read(uint8_t* outBuffer, uint32_t bytesToRead) override; + void write(const uint8_t* buffer, uint32_t bytesToWrite) override; + + /** + * @brief This function reads the current byte without advancing the internal pointer by 1 byte + * + * @return uint8_t + */ + uint8_t peek() override; + + /** + * @brief This function advances the internal pointer by the specified numBytes. + * This can be considered a relative seek + * + */ + bool advance(uint32_t numBytes = 1) override; + bool rewind(uint32_t numBytes = 1) override; + + /** + * @brief This function seeks to the specified absolute save file/buffer offset + * + * @param absoluteOffset + */ + bool seek(uint32_t absoluteOffset) override; + + /** + * @brief Returns the index of the current bank + */ + uint8_t getCurrentBankIndex() const override; +protected: +private: + TransferPakManager& pakManager_; + uint32_t sramOffset_; +}; + +#endif \ No newline at end of file diff --git a/include/widget/CursorWidget.h b/include/widget/CursorWidget.h new file mode 100755 index 0000000..6f04d1a --- /dev/null +++ b/include/widget/CursorWidget.h @@ -0,0 +1,69 @@ +#ifndef _CURSORWIDGET_H +#define _CURSORWIDGET_H + +#include "widget/IWidget.h" +#include "animations/MoveAnimation.h" +#include "core/Sprite.h" + +class AnimationManager; + +typedef struct CursorStyle +{ + sprite_t* sprite; + SpriteRenderSettings spriteSettings; + Rectangle idleMoveDiff; + uint16_t idleAnimationDurationInMs; + uint16_t moveAnimationDurationInMs; +} CursorStyle; + +/** + * @brief This widget represents the cursor in a list. + * It can be used to point to the selected item in a list. + * It's up to the "user"/dev to implement a way (hint: IFocusListener) to move + * this cursor when the focus changes. The reason for this is that the Cursor may + * need to be drawn at a scene-specific offset or any other scene-specific shenanigans. + * By not handling the focus change behaviour inside this widget, we're not restricting the "users" + * of this widget + */ +class CursorWidget : public IWidget +{ +public: + CursorWidget(AnimationManager& animManager); + virtual ~CursorWidget(); + + bool isFocused() const override; + void setFocused(bool isFocused) override; + + bool isVisible() const override; + void setVisible(bool visible) override; + + Rectangle getBounds() const override; + void setBounds(const Rectangle& bounds) override; + + /** + * @brief This function animates a move to the specified bounds + */ + void moveToBounds(const Rectangle& targetBounds); + + Dimensions getSize() const override; + + bool handleUserInput(const joypad_inputs_t& userInput) override; + void render(RDPQGraphics& gfx, const Rectangle& parentBounds) override; + + void setStyle(const CursorStyle& style); + + /** + * @brief This function is called when our moveAnimation has finished + */ + void onMoveAnimationFinished(); +protected: +private: + MoveAnimation idleAnimation_; + MoveAnimation moveAnimation_; + AnimationManager& animManager_; + CursorStyle style_; + Rectangle bounds_; + bool visible_; +}; + +#endif \ No newline at end of file diff --git a/include/widget/DialogWidget.h b/include/widget/DialogWidget.h new file mode 100755 index 0000000..5495a8f --- /dev/null +++ b/include/widget/DialogWidget.h @@ -0,0 +1,118 @@ +#ifndef _DIALOGWIDGET_H +#define _DIALOGWIDGET_H + +#include "widget/IWidget.h" +#include "core/Sprite.h" +#include "core/RDPQGraphics.h" + +#define DIALOG_TEXT_SIZE 512 + +class AnimationManager; + +typedef struct DialogData +{ + char text[DIALOG_TEXT_SIZE]; + // optional sprite of a character that is saying the dialog text + sprite_t* characterSprite; + SpriteRenderSettings characterSpriteSettings; + // bounds of the character sprite relative to the widget + Rectangle characterSpriteBounds; + bool characterSpriteVisible; + sprite_t* buttonSprite; + SpriteRenderSettings buttonSpriteSettings; + // bounds of the button sprite relative to the widget + Rectangle buttonSpriteBounds; + bool buttonSpriteVisible; + // The next Dialog + struct DialogData* next; + bool shouldReleaseWhenDone; + bool userAdvanceBlocked; + //TODO: dialog sound +} DialogData; + +typedef struct DialogWidgetStyle +{ + sprite_t* backgroundSprite; + SpriteRenderSettings backgroundSpriteSettings; + TextRenderSettings textSettings; + int marginLeft; + int marginRight; + int marginTop; + int marginBottom; +} DialogWidgetStyle; + +/** + * This widget is used to display dialog text (usually at the bottom of the screen) + * You can specify a dialog sequence with the DialogData struct to be shown to the user. + * + * When the dialog has finished (after the user presses A when the last DialogData entry was shown) + * the onDialogFinished callback function (if any) will be triggered. + * + * If you press the A button, the DialogWidget advances to the next DialogData entry (if any) + * or (like I said before) triggers the onDialogFinished callback. + */ +class DialogWidget : public IWidget +{ +public: + DialogWidget(AnimationManager& animationManager); + virtual ~DialogWidget(); + + const DialogWidgetStyle& getStyle() const; + void setStyle(const DialogWidgetStyle& style); + + void setData(DialogData* data); + void appendDialogData(DialogData* data); + + bool isFocused() const override; + void setFocused(bool isFocused) override; + + bool isVisible() const override; + void setVisible(bool visible) override; + + Rectangle getBounds() const override; + void setBounds(const Rectangle& bounds) override; + Dimensions getSize() const override; + + /** + * @brief Sets a callback function that will be called when we run out of dialog + */ + void setOnDialogFinishedCallback(void (*onDialogFinishedCb)(void*), void* context); + + /** + * @brief Advances the current dialog -> the next DialogData entry (if any) will be shown + * or the onDialogFinished callback will be triggered + */ + void advanceDialog(); + + bool handleUserInput(const joypad_inputs_t& userInput) override; + void render(RDPQGraphics& gfx, const Rectangle& parentBounds) override; +protected: +private: + /** + * @brief Indicates if the user is allowed to advance the dialog (yet) + * This could be used -for example- to restrict advancing until after a certain amount of time + * For example: waiting until a sound has played. (not implemented yet though) + */ + bool isAdvanceAllowed() const; + + AnimationManager& animationManager_; + Rectangle bounds_; + DialogWidgetStyle style_; + DialogData* data_; + void (*onDialogFinishedCb_)(void*); + void *onDialogFinishedCbContext_; + bool focused_; + bool visible_; + bool btnAPressedOnPrevCheck_; +}; + +/** + * @brief This function sets the text field of the DialogData struct with snprintf + * + * @param data the DialogData struct to fill + * @param format the printf format string + * @param ... variable arguments to use within the snprintf call + */ +void setDialogDataText(DialogData& data, const char* format, ...); + +#endif \ No newline at end of file diff --git a/include/widget/IFocusListener.h b/include/widget/IFocusListener.h new file mode 100755 index 0000000..2e6b854 --- /dev/null +++ b/include/widget/IFocusListener.h @@ -0,0 +1,49 @@ +#ifndef _IFOCUSLISTENER_H +#define _IFOCUSLISTENER_H + +#include "core/common.h" + +class IWidget; + +/** + * @brief Provides metadata on the changed focus + */ +typedef struct FocusChangeStatus +{ + /** + * @brief These are the bounds of the newly focused widget + */ + Rectangle focusBounds; + /** + * @brief The previously focused widget + */ + IWidget* prevFocus; + + /** + * @brief The newly focused widget + */ + IWidget* curFocus; + +} FocusChangeStatus; + +/** + * @brief This interface must be implemented by classes that are interested in + * getting focus change information from a (Vertical)List widget. + * + * Implementing this interface allows the class to register itself onto a List widget. + */ +class IFocusListener +{ +public: + virtual ~IFocusListener(); + + /** + * @brief This callback function will be triggered by the (Vertical)List widget + * whenever the user switches the focus of list entries + */ + virtual void focusChanged(const FocusChangeStatus& status) = 0; +protected: +private: +}; + +#endif \ No newline at end of file diff --git a/include/widget/IWidget.h b/include/widget/IWidget.h new file mode 100755 index 0000000..95983c7 --- /dev/null +++ b/include/widget/IWidget.h @@ -0,0 +1,75 @@ +#ifndef _IWIDGET_H +#define _IWIDGET_H + +#include "core/common.h" +#include + +class RDPQGraphics; + +/** + * This interface class will be used for every UI widget on screen to expose common APIs + */ +class IWidget +{ +public: + /** + * @brief Returns whether the widget is currently focused + */ + virtual bool isFocused() const = 0; + + /** + * @brief Sets whether the widget is currently focused + * + */ + virtual void setFocused(bool isFocused) = 0; + + /** + * @brief Returns whether the widget is currently visible + */ + virtual bool isVisible() const = 0; + + /** + * @brief Changes the visibility of the widget + */ + virtual void setVisible(bool visible) = 0; + + /** + * @brief Returns the current (relative) bounds of the widget + */ + virtual Rectangle getBounds() const = 0; + + /** + * @brief Changes the current (relative) bounds of the widget + */ + virtual void setBounds(const Rectangle& bounds) = 0; + + /** + * @brief Returns the size (width/height) of the widget + */ + virtual Dimensions getSize() const = 0; + + /** + * @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. + */ + virtual bool handleUserInput(const joypad_inputs_t& userInput) = 0; + + /** + * @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! + */ + virtual void render(RDPQGraphics& gfx, const Rectangle& parentBounds) = 0; +protected: +private: +}; + +#endif diff --git a/include/widget/ListItemFiller.h b/include/widget/ListItemFiller.h new file mode 100755 index 0000000..609228d --- /dev/null +++ b/include/widget/ListItemFiller.h @@ -0,0 +1,53 @@ +#ifndef _LISTITEMFILLER_H +#define _LISTITEMFILLER_H + +#include +#include + +/** + * This template class simply serves the purpose of filling the specified ListType with widgets of the given ListItemWidgetType by creating + * these list item widgets based on a list of ListDataType entries and a ListItemWidgetStyleType + * + * ListType must have a function called addWidget(ListItemWidgetType) + * ListItemWidgetStyleType must have a setData(ListDataType) function + * AND a setStyle(ListItemWidgetStyleType) function + * + * ... in order to be able to use the ListItemFiller template class + */ +template +class ListItemFiller +{ +public: + ListItemFiller(ListType& list) + : list_(list) + , widgets_() + { + } + + ~ListItemFiller() + { + for(ListItemWidgetType* item : widgets_) + { + delete item; + } + widgets_.clear(); + } + + void addItems(ListDataType* dataList, size_t dataListSize, const MenuItemStyle& itemStyle) + { + ListItemWidgetType* itemWidget; + for(size_t i = 0; i < dataListSize; ++i) + { + itemWidget = new ListItemWidgetType(); + itemWidget->setData(dataList[i]); + itemWidget->setStyle(itemStyle); + list_.addWidget(itemWidget); + } + } +protected: +private: + ListType& list_; + std::vector widgets_; +}; + +#endif diff --git a/include/widget/MenuItemWidget.h b/include/widget/MenuItemWidget.h new file mode 100755 index 0000000..c66e11c --- /dev/null +++ b/include/widget/MenuItemWidget.h @@ -0,0 +1,123 @@ +#ifndef _MENUITEMWIDGET_H +#define _MENUITEMWIDGET_H + +#include "widget/IWidget.h" +#include "core/Sprite.h" +#include "core/RDPQGraphics.h" + +#include + +/** + * The data struct that will be shown by MenuItemWidget + */ +typedef struct MenuItemData +{ + /** + * menu item text/title + */ + const char* title; + /** + * function pointer to a callback function that will handle the "confirm" action + */ + void (*onConfirmAction)(void* context, const void* itemParam); + /** + * A user context that will be passed to the onConfirmAction() callback when called + */ + void* context; + + /** + * An additional user param which will be passed to the onConfirmAction callback + */ + const void* itemParam; +} MenuItemData; + +/** + * a style struct that describes the style of the MenuItemWidget + */ +typedef struct MenuItemStyle +{ + /** + * width and height for the MenuItemWidget + */ + Dimensions size; + /** + * (optional) background sprite + */ + sprite_t* backgroundSprite; + /* + * RenderSettings that influence how the backgroundSprite is + * being rendered + */ + SpriteRenderSettings backgroundSpriteSettings; + + /** + * (optional) icon sprite + */ + sprite_t* iconSprite; + + /** + * RenderSettings that influence how the iconSprite is being rendered + */ + SpriteRenderSettings iconSpriteSettings; + + /** + * relative bounds of the icon sprite in relation to the MenuItem widget + */ + Rectangle iconSpriteBounds; + /** + * These are the text settings for when the MenuItemWidget is NOT focused by the user + */ + TextRenderSettings titleNotFocused; + /** + * These are the text render settings for when the MenuItemWidget is focused by the user + */ + TextRenderSettings titleFocused; + /** + * Offset to indicate how far from the left we need to start rendering the title text + */ + uint16_t leftMargin; + /** + * Offset to indicate how far from the top we need to start rendering the title text + */ + uint16_t topMargin; +} MenuItemStyle; + +/** + * This is a widget created for displaying inside a VerticalList widget to show a menu item + */ +class MenuItemWidget : public IWidget +{ +public: + MenuItemWidget(); + virtual ~MenuItemWidget(); + + void setData(const MenuItemData& data); + void setStyle(const MenuItemStyle& style); + + bool isFocused() const override; + void setFocused(bool isFocused) override; + + bool isVisible() const override; + void setVisible(bool visible) override; + + Rectangle getBounds() const override; + void setBounds(const Rectangle& bounds); + + Dimensions getSize() const override; + + bool handleUserInput(const joypad_inputs_t& userInput) override; + void render(RDPQGraphics& gfx, const Rectangle& parentBounds) override; +protected: + /** + * Executes the onConfirmAction callback (if any) + */ + void execute(); +private: + MenuItemData data_; + MenuItemStyle style_; + bool focused_; + bool visible_; + bool aButtonPressed_; +}; + +#endif diff --git a/include/widget/TransferPakDetectionWidget.h b/include/widget/TransferPakDetectionWidget.h new file mode 100755 index 0000000..1b8d65b --- /dev/null +++ b/include/widget/TransferPakDetectionWidget.h @@ -0,0 +1,148 @@ +#ifndef _TRANSFERPAKDETECTIONWIDGET_H +#define _TRANSFERPAKDETECTIONWIDGET_H + +#include "widget/IWidget.h" +#include "core/Sprite.h" +#include "core/RDPQGraphics.h" +#include "gen1/Gen1Common.h" +#include "gen2/Gen2Common.h" + +class AnimationManager; +class TransferPakManager; + +enum class TransferPakWidgetState +{ + UNKNOWN, + DETECTING_PAK, + VALIDATING_GB_HEADER, + DETECTING_GAME, + GB_HEADER_VALIDATION_FAILED, + NO_TRANSFER_PAK_FOUND, + NO_GAME_FOUND, + GAME_FOUND +}; + +typedef struct TransferPakDetectionWidgetStyle +{ + TextRenderSettings textSettings; +} TransferPakDetectionWidgetStyle; + +/** + * @brief This widget is used to handle the transfer pak detection process. + * One of the major reasons for this widget is because libdragon's joypad polling is done in the background during interrupts + * and therefore may not be ready when we're initializing the scene. Best thing to do is to wait for key input + * + * So I might as well turn it into something visible. + */ +class TransferPakDetectionWidget : public IWidget +{ +public: + TransferPakDetectionWidget(AnimationManager& animManager, TransferPakManager& pakManager); + virtual ~TransferPakDetectionWidget(); + + 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; + + TransferPakWidgetState getState() const; + + /** + * This function retrieves the gametypes for gen1 and gen2. The hard split is because of the way libpokemegb was set up + */ + void retrieveGameType(Gen1GameType& outGen1Type, Gen2GameType& outGen2Type); + + void setStyle(const TransferPakDetectionWidgetStyle& style); + void setStateChangedCallback(void (*callback)(void*, TransferPakWidgetState), void* context); +protected: +private: + void switchState(TransferPakWidgetState previousState, TransferPakWidgetState newState); + + void renderUnknownState(RDPQGraphics& gfx, const Rectangle& parentBounds); + void renderErrorState(RDPQGraphics& gfx, const Rectangle& parentBounds); + + /** + * @brief This function checks every controller for an N64 transfer pak, selects it in our + * TransferPakManager instance and returns true after it finds the first one. + */ + bool selectTransferPak(); + + /** + * @brief Checks whether the CRC value of the gameboy header matches. + * This check is useful for checking whether a good connection with the cartridge was established. + */ + bool validateGameboyHeader(); + + /** + * @brief This function detects the Pokémon game type in the N64 transfer pak. + * It stores the found value in the gen1Type_ and gen2Type_ member vars. These values can be retrieved with retrieveGameType() + * + * @return returns true if the connected game pak is a Gen1 or Gen2 pokémon gameboy game. + */ + bool detectGameType(); + + TransferPakDetectionWidgetStyle style_; + AnimationManager& animManager_; + TransferPakManager& tpakManager_; + Rectangle bounds_; + Rectangle textBounds_; + TransferPakWidgetState currentState_; + joypad_inputs_t previousInputState_; + Gen1GameType gen1Type_; + Gen2GameType gen2Type_; + void (*stateChangedCallback_)(void*, TransferPakWidgetState); + void* stateChangedCallbackContext_; + bool focused_; + bool visible_; +}; + +#endif \ No newline at end of file diff --git a/include/widget/VerticalList.h b/include/widget/VerticalList.h new file mode 100755 index 0000000..f4a70f5 --- /dev/null +++ b/include/widget/VerticalList.h @@ -0,0 +1,162 @@ +#ifndef _VERTICALLIST_H +#define _VERTICALLIST_H + +#include "animations/IAnimation.h" +#include "widget/IWidget.h" +#include "core/Sprite.h" + +#include + +class RDPQGraphics; +class IWidget; +class VerticalList; +class AnimationManager; + +typedef std::vector IWidgetList; +typedef std::vector WidgetBoundsList; + +struct FocusChangeStatus; +class IFocusListener; +typedef std::vector FocusListenerList; + +/** + * @brief This Animation implementation is used internal in VerticalList + * to move the "view window"'s y-coordinate. + * + * It's specific to the VerticalList widget + */ +class MoveVerticalListWindowAnimation : public AbstractAnimation +{ +public: + MoveVerticalListWindowAnimation(VerticalList* list); + virtual ~MoveVerticalListWindowAnimation(); + + AnimationDistanceTimeFunctionType getDistanceTimeFunctionType() const override; + + uint32_t getDurationInMs() const override; + + void start(uint32_t windowStartY, uint32_t windowEndY); +protected: + void apply(float pos) override; +private: + VerticalList* list_; + int32_t windowStartY_; + int32_t windowEndY_; +}; + +/** + * @brief This struct defines the "style" of the widget. This means + * that it defines properties that influence the visual characteristics + * of the entire list component. + * + * Use verticalList.setStyle() to apply a style onto the list. + */ +typedef struct VerticalListStyle +{ + /** + * @brief (optional) a background sprite to render the background of + * the widget + */ + sprite_t* backgroundSprite; + /** + * @brief (optional) render settings for rendering the background sprite (if any) + * + */ + SpriteRenderSettings backgroundSpriteSettings; + /** + * @brief left margin -> the widgets will start rendering after this x spacing offset from the left edge of the list + */ + int marginLeft; + /** + * @brief right margin -> the widgets will stop rendering x pixels from the right of this list + */ + int marginRight; + /** + * @brief top margin -> the widgets will start rendering after this y spacing offset from the top edge of the list + * + */ + int marginTop; + /** + * @brief bottom margin -> the widgets will stop rendering y pixels from the bottom edge of this list. + * + */ + int marginBottom; + + /** + * @brief the amount of spacing (in pixels) between 2 list widgets (default: 0) + */ + int verticalSpacingBetweenWidgets; +} VerticalListStyle; + +/** + * @brief This widget implements a vertical list. + * It can be used for menu-like UI elements. + * + * It's very flexible in the sense that you can use it for any kind of IWidget. + * + * You can influence spacing and background with the VerticalListStyle. + * + * In concept, internally it keeps an infinitely growable list of widgets of which the + * vertical y-coordinate will be increased with each widget. These coordinates remain static. + * + * However, it also has a "view window" that is used to translate the widget coordinates to the actual + * coordinates the visible widgets need to be rendered at. + * + * A vertical move to a widget that is outside of the view window is implemented with an animation of + * the view window y-coordinate. + */ +class VerticalList : public IWidget +{ +public: + VerticalList(AnimationManager& animationManager); + virtual ~VerticalList(); + + bool focusNext(); + bool focusPrevious(); + + void addWidget(IWidget* widget); + void clearWidgets(); + + void setStyle(const VerticalListStyle& style); + void setViewWindowStartY(uint32_t windowStartY); + + bool isFocused() const override; + void setFocused(bool isFocused) override; + + bool isVisible() const override; + void setVisible(bool visible) override; + + Rectangle getBounds() const override; + void setBounds(const Rectangle& bounds) override; + Dimensions getSize() const override; + + bool handleUserInput(const joypad_inputs_t& userInput) override; + void render(RDPQGraphics& gfx, const Rectangle& parentBounds) override; + + void registerFocusListener(IFocusListener* listener); + void unregisterFocusListener(IFocusListener* listener); +protected: +private: + void rebuildLayout(); + /** + * Scrolls the window until the focused widget is fully visible with an animation + * @return returns the number of vertical pixels that will be scrolled + */ + int32_t scrollWindowToFocusedWidget(); + void moveWindow(int32_t yAmount); + void notifyFocusListeners(const FocusChangeStatus& status); + + MoveVerticalListWindowAnimation moveWindowAnimation_; + IWidgetList widgetList_; + WidgetBoundsList widgetBoundsList_; + FocusListenerList focusListeners_; + VerticalListStyle listStyle_; + Rectangle bounds_; + uint32_t windowMinY_; + uint32_t focusedWidgetIndex_; + AnimationManager& animManager_; + bool focused_; + bool visible_; +}; + +#endif \ No newline at end of file diff --git a/libdragon b/libdragon new file mode 160000 index 0000000..fd34d09 --- /dev/null +++ b/libdragon @@ -0,0 +1 @@ +Subproject commit fd34d090adab1858815cf8e2c035ed4cd5e283a6 diff --git a/libpokemegb b/libpokemegb new file mode 160000 index 0000000..747bdd4 --- /dev/null +++ b/libpokemegb @@ -0,0 +1 @@ +Subproject commit 747bdd4b4b27c8d1bd9c44f0d651d4c8c439b90c diff --git a/src/animations/AnimationManager.cpp b/src/animations/AnimationManager.cpp new file mode 100755 index 0000000..61b868a --- /dev/null +++ b/src/animations/AnimationManager.cpp @@ -0,0 +1,37 @@ +#include "animations/AnimationManager.h" +#include "animations/IAnimation.h" + +#include + +static float normalizeTimeStep(IAnimation* anim, uint32_t elapsedTimeInMs) +{ + return static_cast(elapsedTimeInMs) / anim->getDurationInMs(); +} + +void AnimationManager::add(IAnimation* animation) +{ + animations_.push_back(animation); +} + +void AnimationManager::remove(IAnimation* animation) +{ + auto it = std::find(animations_.begin(), animations_.end(), animation); + if(it != animations_.end()) + { + animations_.erase(it); + } +} + +void AnimationManager::step(uint32_t elapsedTimeInMs) +{ + float animationStep; + + for(IAnimationList::iterator it = animations_.begin(); it != animations_.end(); ++it) + { + if(!(*it)->isFinished() && (*it)->getDurationInMs() > 0) + { + animationStep = normalizeTimeStep((*it), elapsedTimeInMs); + (*it)->step(animationStep); + } + } +} \ No newline at end of file diff --git a/src/animations/IAnimation.cpp b/src/animations/IAnimation.cpp new file mode 100755 index 0000000..993cef9 --- /dev/null +++ b/src/animations/IAnimation.cpp @@ -0,0 +1,117 @@ +#include "animations/IAnimation.h" + +static float calculateAnimationPos(AnimationDistanceTimeFunctionType distanceTimeType, float timePos) +{ + float result; + + switch(distanceTimeType) + { + case AnimationDistanceTimeFunctionType::NONE: + return 1.f; + case AnimationDistanceTimeFunctionType::LINEAR: + result = timePos; + break; + case AnimationDistanceTimeFunctionType::EASE_IN_EASE_OUT: + // using the bezier solution here: https://stackoverflow.com/questions/13462001/ease-in-and-ease-out-animation-formula + result = timePos * timePos * (3.0f - 2.0f * timePos); + break; + default: + result = 0.f; + break; + } + + return result; +} + +IAnimation::~IAnimation() +{ +} + +AbstractAnimation::AbstractAnimation(float initialTimePos) + : currentTimePos_(initialTimePos) + , animationFinishedCb_(nullptr) + , animationFinishedCallbackContext_(nullptr) + , loopType_(AnimationLoopType::NONE) + , isStepIncreasing_(true) +{ +} + +AbstractAnimation::~AbstractAnimation() +{ +} + +void AbstractAnimation::step(float stepSize, bool suppressFinishedCallback) +{ + if(isStepIncreasing_) + { + currentTimePos_ += stepSize; + if(currentTimePos_ >= 1.f) + { + switch(loopType_) + { + case AnimationLoopType::NONE: + currentTimePos_ = 1.f; + break; + case AnimationLoopType::NORMAL_LOOP: + currentTimePos_ = 0.f; + break; + case AnimationLoopType::BACK_AND_FORTH: + currentTimePos_ = 1.f; + isStepIncreasing_ = false; + break; + } + } + } + else + { + currentTimePos_ -= stepSize; + if(currentTimePos_ <= 0.f) + { + // reached the beginning of the animation, so start moving in the forward direction again. + currentTimePos_ = 0.f; + // We can only be in the !isStepIncreasing_ state if AnimationLoopType == BACK_AND_FORTH + isStepIncreasing_ = true; + } + } + + const float animPos = calculateAnimationPos(getDistanceTimeFunctionType(), currentTimePos_); + apply(animPos); + + // call the animationFinished callback if set and applicable + if(animationFinishedCb_ && !suppressFinishedCallback && isFinished()) + { + animationFinishedCb_(animationFinishedCallbackContext_); + } +} + +void AbstractAnimation::skipToEnd() +{ + if(isFinished()) + { + return; + } + // the step function will autocorrect to 1.f total + // when we call the skipToEnd() function, we don't want to fire the animation finished callback + step(1.f, true); +} + +bool AbstractAnimation::isFinished() const +{ + return (loopType_ == AnimationLoopType::NONE) && (currentTimePos_ >= 1.f); +} + +AnimationLoopType AbstractAnimation::getLoopType() const +{ + return loopType_; +} + +void AbstractAnimation::setLoopType(AnimationLoopType loopType) +{ + loopType_ = loopType; +} + +void AbstractAnimation::setAnimationFinishedCallback(void* context, void (*animationFinishedCb)(void*)) +{ + animationFinishedCb_ = animationFinishedCb; + animationFinishedCallbackContext_ = context; +} \ No newline at end of file diff --git a/src/animations/MoveAnimation.cpp b/src/animations/MoveAnimation.cpp new file mode 100755 index 0000000..2fe39bb --- /dev/null +++ b/src/animations/MoveAnimation.cpp @@ -0,0 +1,52 @@ +#include "animations/MoveAnimation.h" +#include "widget/IWidget.h" + +#include + +MoveAnimation::MoveAnimation(IWidget* target) + : AbstractAnimation(1.f) + , target_(target) + , startBounds_({0}) + , diffStartEnd_({0}) + , durationInMs_(0) +{ +} + +MoveAnimation::~MoveAnimation() +{ +} + +AnimationDistanceTimeFunctionType MoveAnimation::getDistanceTimeFunctionType() const +{ + return AnimationDistanceTimeFunctionType::EASE_IN_EASE_OUT; +} + +uint32_t MoveAnimation::getDurationInMs() const +{ + return durationInMs_; +} + +void MoveAnimation::start(const Rectangle& startBounds, const Rectangle& moveVectorStartEnd, uint32_t durationInMs) +{ + currentTimePos_ = 0.f; + startBounds_ = startBounds; + diffStartEnd_ = moveVectorStartEnd; + durationInMs_ = durationInMs; +} + +void MoveAnimation::reset() +{ + currentTimePos_ = 0.f; +} + +void MoveAnimation::apply(float pos) +{ + const Rectangle newBounds = { + .x = startBounds_.x + static_cast(ceilf(pos * diffStartEnd_.x)), + .y = startBounds_.y + static_cast(ceilf(pos * diffStartEnd_.y)), + .width = startBounds_.width + static_cast(ceilf(pos * diffStartEnd_.width)), + .height = startBounds_.height + static_cast(ceilf(pos * diffStartEnd_.height)) + }; + + target_->setBounds(newBounds); +} \ No newline at end of file diff --git a/src/core/Application.cpp b/src/core/Application.cpp new file mode 100755 index 0000000..ab4d333 --- /dev/null +++ b/src/core/Application.cpp @@ -0,0 +1,59 @@ +#include "core/Application.h" +#include "scenes/IScene.h" + +Application::Application() + : graphics_() + , animationManager_() + , fontManager_() + , tpakManager_() + , sceneManager_(graphics_, animationManager_, fontManager_, tpakManager_) + , sceneBounds_({0}) +{ +} + +Application::~Application() +{ + graphics_.destroy(); + + display_close(); + timer_close(); + joypad_close(); +} + +void Application::init() +{ + // Based on example code https://github.com/DragonMinded/libdragon/wiki/OpenGL-on-N64 + debug_init_isviewer(); + //console_set_debug(true); + + joypad_init(); + timer_init(); + + dfs_init(DFS_DEFAULT_LOCATION); + + graphics_.init(); + display_init(RESOLUTION_320x240, DEPTH_16_BPP, 3, GAMMA_NONE, FILTERS_RESAMPLE); + sceneBounds_ = {.x = 0, .y = 0, .width = 320, .height = 240 }; + //display_init(RESOLUTION_320x240, DEPTH_16_BPP, 3, GAMMA_NONE, ANTIALIAS_RESAMPLE_FETCH_ALWAYS); + + sceneManager_.switchScene(SceneType::INIT_TRANSFERPAK); +} + +void Application::run() +{ + while(1) + { + graphics_.beginFrame(); + + //const uint64_t before = get_ticks(); + animationManager_.step(20); + joypad_poll(); + sceneManager_.handleUserInput(); + + sceneManager_.render(sceneBounds_); + //const uint64_t after = get_ticks(); + //debugf("frame took %lu ms\r\n", static_cast(TICKS_TO_MS(after - before))); + + graphics_.finishAndShowFrame(); + } +} diff --git a/src/core/DragonUtils.cpp b/src/core/DragonUtils.cpp new file mode 100755 index 0000000..c62f083 --- /dev/null +++ b/src/core/DragonUtils.cpp @@ -0,0 +1,48 @@ +#include "core/DragonUtils.h" + +static uint8_t ANALOG_STICK_THRESHOLD = 30; + +const UINavigationKey determineUINavigationKey(joypad_inputs_t inputs, NavigationInputSourceType sourceType) +{ + if(sourceType == NavigationInputSourceType::ANALOG_STICK || sourceType == NavigationInputSourceType::BOTH) + { + const int8_t absXVal = static_cast(abs(inputs.stick_x)); + const int8_t absYVal = static_cast(abs(inputs.stick_y)); + + if(absXVal > absYVal) + { + if(absXVal >= ANALOG_STICK_THRESHOLD) + { + return (inputs.stick_x < 0) ? UINavigationKey::LEFT : UINavigationKey::RIGHT; + } + } + else + { + if(absYVal >= ANALOG_STICK_THRESHOLD) + { + return (inputs.stick_y < 0) ? UINavigationKey::DOWN : UINavigationKey::UP; + } + } + } + + if(sourceType == NavigationInputSourceType::DPAD || sourceType == NavigationInputSourceType::BOTH) + { + if(inputs.btn.d_down) + { + return UINavigationKey::DOWN; + } + if(inputs.btn.d_up) + { + return UINavigationKey::UP; + } + if(inputs.btn.d_left) + { + return UINavigationKey::LEFT; + } + if(inputs.btn.d_right) + { + return UINavigationKey::RIGHT; + } + } + return UINavigationKey::NONE; +} \ No newline at end of file diff --git a/src/core/FontManager.cpp b/src/core/FontManager.cpp new file mode 100755 index 0000000..7c6dc79 --- /dev/null +++ b/src/core/FontManager.cpp @@ -0,0 +1,36 @@ +#include "core/FontManager.h" + +FontManager::FontManager() + : fontMap_() + , nextFontId_(1) +{ +} + +uint8_t FontManager::getFont(const char* fontUri) +{ + auto it = fontMap_.find(std::string(fontUri)); + if(it == fontMap_.end()) + { + FontEntry entry = { + .font = rdpq_font_load(fontUri), + .fontId = nextFontId_ + }; + fontMap_.emplace(fontUri, entry); + + rdpq_text_register_font(entry.fontId, entry.font); + return entry.fontId; + } + return it->second.fontId; +} + +void FontManager::registerFontStyle(uint8_t fontId, uint8_t fontStyleId, const rdpq_fontstyle_t& fontStyle) +{ + for(auto it = fontMap_.begin(); it != fontMap_.end(); ++it) + { + if(it->second.fontId == fontId) + { + rdpq_font_style(it->second.font, fontStyleId, &fontStyle); + return; + } + } +} \ No newline at end of file diff --git a/src/core/RDPQGraphics.cpp b/src/core/RDPQGraphics.cpp new file mode 100755 index 0000000..18b2f3b --- /dev/null +++ b/src/core/RDPQGraphics.cpp @@ -0,0 +1,299 @@ +#include "core/RDPQGraphics.h" +#include "core/Sprite.h" + +static void render_sprite_normal(const Rectangle &dstRect, sprite_t *sprite, const SpriteRenderSettings &renderSettings) +{ + if (!isZeroSizeRectangle(renderSettings.srcRect)) + { + const rdpq_blitparms_t blitParams = { + .s0 = renderSettings.srcRect.x, + .t0 = renderSettings.srcRect.y, + .width = renderSettings.srcRect.width, + .height = renderSettings.srcRect.height, + .scale_x = static_cast(dstRect.width) / renderSettings.srcRect.width, + .scale_y = static_cast(dstRect.height) / renderSettings.srcRect.height, + .theta = renderSettings.rotationAngle + }; + + rdpq_sprite_blit(sprite, dstRect.x, dstRect.y, &blitParams); + } + else + { + const rdpq_blitparms_t blitParams = { + .scale_x = static_cast(dstRect.width) / sprite->width, + .scale_y = static_cast(dstRect.height) / sprite->height, + .theta = renderSettings.rotationAngle + }; + + rdpq_sprite_blit(sprite, dstRect.x, dstRect.y, &blitParams); + } +} + +static void render_sprite_ninegrid(const Rectangle &dstRect, sprite_t *sprite, const SpriteRenderSettings &renderSettings) +{ + // left top corner + Rectangle curDest = { + .x = dstRect.x, + .y = dstRect.y, + .width = renderSettings.srcRect.x, + .height = renderSettings.srcRect.y + }; + Rectangle curSrc = { + .x = 0, + .y = 0, + .width = renderSettings.srcRect.x, + .height = renderSettings.srcRect.y + }; + render_sprite_normal(curDest, sprite, {.renderMode = SpriteRenderMode::NORMAL, .srcRect = curSrc}); + + // top edge + curDest = { + .x = dstRect.x + renderSettings.srcRect.x, + .y = dstRect.y, + .width = dstRect.width - renderSettings.srcRect.width - renderSettings.srcRect.x, + .height = renderSettings.srcRect.y + }; + curSrc = { + .x = renderSettings.srcRect.x, + .y = 0, + .width = sprite->width - renderSettings.srcRect.width - renderSettings.srcRect.x, + .height = renderSettings.srcRect.y + }; + render_sprite_normal(curDest, sprite, {.renderMode = SpriteRenderMode::NORMAL, .srcRect = curSrc}); + + // right top corner + curDest = { + .x = dstRect.x + dstRect.width - renderSettings.srcRect.width, + .y = dstRect.y, + .width = renderSettings.srcRect.width, + .height = renderSettings.srcRect.y + }; + curSrc = { + .x = sprite->width - renderSettings.srcRect.width, + .y = 0, + .width = renderSettings.srcRect.width, + .height = renderSettings.srcRect.y + }; + render_sprite_normal(curDest, sprite, {.renderMode = SpriteRenderMode::NORMAL, .srcRect = curSrc}); + + // right edge + curDest = { + .x = dstRect.x + dstRect.width - renderSettings.srcRect.width, + .y = dstRect.y + renderSettings.srcRect.y, + .width = renderSettings.srcRect.width, + .height = dstRect.height - renderSettings.srcRect.height - renderSettings.srcRect.y + }; + curSrc = { + .x = sprite->width - renderSettings.srcRect.width, + .y = renderSettings.srcRect.y, + .width = renderSettings.srcRect.width, + .height = sprite->height - renderSettings.srcRect.height - renderSettings.srcRect.y + }; + render_sprite_normal(curDest, sprite, {.renderMode = SpriteRenderMode::NORMAL, .srcRect = curSrc}); + + // bottom right corner + curDest = { + .x = dstRect.x + dstRect.width - renderSettings.srcRect.width, + .y = dstRect.y + dstRect.height - renderSettings.srcRect.height, + .width = renderSettings.srcRect.width, + .height = renderSettings.srcRect.height + }; + curSrc = { + .x = sprite->width - renderSettings.srcRect.width, + .y = sprite->height - renderSettings.srcRect.height, + .width = renderSettings.srcRect.width, + .height = renderSettings.srcRect.height + }; + render_sprite_normal(curDest, sprite, {.renderMode = SpriteRenderMode::NORMAL, .srcRect = curSrc}); + + // bottom edge + curDest = { + .x = dstRect.x + renderSettings.srcRect.x, + .y = dstRect.y + dstRect.height - renderSettings.srcRect.height, + .width = dstRect.width - renderSettings.srcRect.width - renderSettings.srcRect.x, + .height = renderSettings.srcRect.height + }; + curSrc = { + .x = renderSettings.srcRect.x, + .y = sprite->height - renderSettings.srcRect.height, + .width = sprite->width - renderSettings.srcRect.width - renderSettings.srcRect.x, + .height = renderSettings.srcRect.height + }; + render_sprite_normal(curDest, sprite, {.renderMode = SpriteRenderMode::NORMAL, .srcRect = curSrc}); + + // bottom left corner + curDest = { + .x = dstRect.x, + .y = dstRect.y + dstRect.height - renderSettings.srcRect.height, + .width = renderSettings.srcRect.x, + .height = renderSettings.srcRect.height + }; + curSrc = { + .x = 0, + .y = sprite->height - renderSettings.srcRect.height, + .width = renderSettings.srcRect.x, + .height = renderSettings.srcRect.height + }; + render_sprite_normal(curDest, sprite, {.renderMode = SpriteRenderMode::NORMAL, .srcRect = curSrc}); + + // left edge + curDest = { + .x = dstRect.x, + .y = dstRect.y + renderSettings.srcRect.y, + .width = renderSettings.srcRect.x, + .height = dstRect.height - renderSettings.srcRect.height - renderSettings.srcRect.y + }; + curSrc = { + .x = 0, + .y = renderSettings.srcRect.y, + .width = renderSettings.srcRect.x, + .height = sprite->height - renderSettings.srcRect.height - renderSettings.srcRect.y + }; + render_sprite_normal(curDest, sprite, {.renderMode = SpriteRenderMode::NORMAL, .srcRect = curSrc}); + + // inner container + curDest = { + .x = dstRect.x + renderSettings.srcRect.x, + .y = dstRect.y + renderSettings.srcRect.y, + .width = dstRect.width - renderSettings.srcRect.width - renderSettings.srcRect.x, + .height = dstRect.height - renderSettings.srcRect.height - renderSettings.srcRect.y + }; + curSrc = { + .x = renderSettings.srcRect.x, + .y = renderSettings.srcRect.y, + .width = sprite->width - renderSettings.srcRect.width - renderSettings.srcRect.x, + .height = sprite->height - renderSettings.srcRect.height - renderSettings.srcRect.y + }; + render_sprite_normal(curDest, sprite, {.renderMode = SpriteRenderMode::NORMAL, .srcRect = curSrc}); +} + +RDPQGraphics::RDPQGraphics() + : clipRect_({0}) + , initialized_(false) + , debugFrameTriggered_(false) +{ +} + +RDPQGraphics::~RDPQGraphics() +{ +} + +void RDPQGraphics::init() +{ + rdpq_init(); + rdpq_debug_start(); + initialized_ = true; +} + +void RDPQGraphics::destroy() +{ + rdpq_debug_stop(); + rdpq_close(); + initialized_ = false; +} + +void RDPQGraphics::triggerDebugFrame() +{ + rdpq_debug_log(true); + debugFrameTriggered_ = true; +} + +void RDPQGraphics::beginFrame() +{ + // Attach and clear the screen + surface_t *disp = display_get(); + rdpq_attach_clear(disp, NULL); +} + +void RDPQGraphics::finishAndShowFrame() +{ + rdpq_detach_show(); + if(debugFrameTriggered_) + { + rdpq_debug_log(false); + debugFrameTriggered_ = false; + } +} + +void RDPQGraphics::fillRectangle(const Rectangle &dstRect, color_t color) +{ + rdpq_mode_push(); + rdpq_set_mode_fill(color); + rdpq_fill_rectangle(dstRect.x, dstRect.y, dstRect.x + dstRect.width, dstRect.y + dstRect.height); + rdpq_mode_pop(); +} + +void RDPQGraphics::drawText(const Rectangle &dstRect, const char *text, const TextRenderSettings& renderSettings) +{ + // disable_interrupts(); + // uint32_t t0 = get_ticks(); + rdpq_textparms_t textParams = { + // .line_spacing = -3, + .style_id = renderSettings.fontStyleId, + .width = static_cast(dstRect.width), + .height = static_cast(dstRect.height), + .align = ALIGN_LEFT, + .valign = VALIGN_TOP, + .char_spacing = renderSettings.charSpacing, + .line_spacing = renderSettings.lineSpacing, + .wrap = WRAP_WORD, + }; + + rdpq_text_print(&textParams, renderSettings.fontId, dstRect.x, dstRect.y, text); + + // TODO: this is a temporary workaround for a bug I reported to the libdragon team on discord on 05/07/2024 + // https://discord.com/channels/205520502922543113/974342113850445874 + // Don't forget to remove it after it has been fixed. + rdpq_sync_tile(); + + // rdpq_paragraph_render(partext, (320-box_width)/2, (240-box_height)/2); + // uint32_t t1 = get_ticks(); + // enable_interrupts(); +} + +void RDPQGraphics::drawSprite(const Rectangle &dstRect, sprite_t *sprite, const SpriteRenderSettings &renderSettings) +{ + rdpq_mode_begin(); + rdpq_set_mode_standard(); + rdpq_mode_alphacompare(1); // colorkey (draw pixel with alpha >= 1) + switch(sprite_get_format(sprite)) + { + case FMT_RGBA32: + case FMT_IA8: + case FMT_IA16: + rdpq_mode_blender(RDPQ_BLENDER_MULTIPLY); + break; + default: + break; + } + rdpq_mode_filter(FILTER_BILINEAR); + rdpq_mode_end(); + + switch (renderSettings.renderMode) + { + case SpriteRenderMode::NORMAL: + render_sprite_normal(dstRect, sprite, renderSettings); + break; + case SpriteRenderMode::NINESLICE: + render_sprite_ninegrid(dstRect, sprite, renderSettings); + break; + default: + break; + } +} + +const Rectangle& RDPQGraphics::getClippingRectangle() const +{ + return clipRect_; +} + +void RDPQGraphics::setClippingRectangle(const Rectangle& clipRect) +{ + clipRect_ = clipRect; + rdpq_set_scissor(clipRect.x, clipRect.y, clipRect.x + clipRect.width, clipRect.y + clipRect.height); +} + +void RDPQGraphics::resetClippingRectangle() +{ + setClippingRectangle({ .x = 0, .y = 0, .width = static_cast(display_get_width()), .height = static_cast(display_get_height()) }); +} \ No newline at end of file diff --git a/src/core/common.cpp b/src/core/common.cpp new file mode 100755 index 0000000..f06c0c0 --- /dev/null +++ b/src/core/common.cpp @@ -0,0 +1,16 @@ +#include "core/common.h" + +bool isZeroSizeRectangle(const Rectangle &rect) +{ + return (!rect.width || !rect.height); +} + +Rectangle addOffset(const Rectangle &a, const Rectangle &b) +{ + return Rectangle{.x = a.x + b.x, .y = a.y + b.y, .width = a.width, .height = a.height}; +} + +Dimensions getDimensions(const Rectangle &r) +{ + return Dimensions{.width = r.width, .height = r.height}; +} \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp new file mode 100755 index 0000000..945472b --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,9 @@ +#include "core/Application.h" + +int main(void) +{ + Application app; + + app.init(); + app.run(); +} \ No newline at end of file diff --git a/src/menu/MenuEntries.cpp b/src/menu/MenuEntries.cpp new file mode 100755 index 0000000..ccf6943 --- /dev/null +++ b/src/menu/MenuEntries.cpp @@ -0,0 +1,41 @@ +#include "menu/MenuEntries.h" +#include "menu/MenuFunctions.h" + +MenuItemData gen1MenuEntries[] = { + { + .title = "Event Pokémon", + .onConfirmAction = goToGen1DistributionPokemonMenu + } +}; + +const uint32_t gen1MenuEntriesSize = sizeof(gen1MenuEntries); + +MenuItemData gen2MenuEntries[] = { + { + .title = "Event Pokémon", + .onConfirmAction = goToGen2DistributionPokemonMenu + }, + { + .title = "PCNY Pokémon", + .onConfirmAction = goToGen2PCNYDistributionPokemonMenu, + } +}; + +const uint32_t gen2MenuEntriesSize = sizeof(gen2MenuEntries); + +MenuItemData gen2CrystalMenuEntries[] = { + { + .title = "Event Pokémon", + .onConfirmAction = goToGen2DistributionPokemonMenu + }, + { + .title = "PCNY Pokémon", + .onConfirmAction = goToGen2PCNYDistributionPokemonMenu, + }, + { + .title = "Receive GS Ball", + .onConfirmAction = gen2ReceiveGSBall + } +}; + +const uint32_t gen2CrystalMenuEntriesSize = sizeof(gen2CrystalMenuEntries); \ No newline at end of file diff --git a/src/menu/MenuFunctions.cpp b/src/menu/MenuFunctions.cpp new file mode 100755 index 0000000..50b151f --- /dev/null +++ b/src/menu/MenuFunctions.cpp @@ -0,0 +1,120 @@ +#include "menu/MenuFunctions.h" +#include "menu/MenuEntries.h" +#include "core/RDPQGraphics.h" +#include "scenes/DistributionPokemonListScene.h" +#include "scenes/SceneManager.h" +#include "gen2/Gen2GameReader.h" +#include "transferpak/TransferPakManager.h" +#include "transferpak/TransferPakRomReader.h" +#include "transferpak/TransferPakSaveManager.h" + +#define POKEMON_CRYSTAL_ITEM_ID_GS_BALL 0x73 + +#if 0 +static void goToMenu(void* context, MenuItemData* menuEntries, uint32_t numMenuEntries) +{ + MenuScene* scene = static_cast(context); + SceneManager& sceneManager = scene->getDependencies().sceneManager; + + MenuSceneContext* sceneContext = new MenuSceneContext{ + .menuEntries = menuEntries, + .numMenuEntries = numMenuEntries + }; + + sceneManager.switchScene(SceneType::MENU, deleteMenuSceneContext, sceneContext); +} +#endif + +static void goToDistributionPokemonListMenu(void* context, DistributionPokemonListType type) +{ + auto sceneContext = new DistributionPokemonListSceneContext; + sceneContext->listType = type; + + MenuScene* scene = static_cast(context); + SceneManager& sceneManager = scene->getDependencies().sceneManager; + + sceneManager.switchScene(SceneType::DISTRIBUTION_POKEMON_LIST, deleteDistributionPokemonListSceneContext, sceneContext); +} + +void printMessage(void* context, const void*) +{ + debugf((const char*)context); +} + +void activateFrameLog(void* context, const void*) +{ + MenuScene* scene = static_cast(context); + RDPQGraphics& gfx = scene->getDependencies().gfx; + debugf("Triggering RDPQ log for 1 frame!\r\n"); + gfx.triggerDebugFrame(); +} + +void goToTestScene(void* context, const void* param) +{ + MenuScene* scene = static_cast(context); + SceneManager& sceneManager = scene->getDependencies().sceneManager; + + sceneManager.switchScene(SceneType::TEST); +} + +void goToGen1DistributionPokemonMenu(void* context, const void*) +{ + goToDistributionPokemonListMenu(context, DistributionPokemonListType::GEN1); + +} + +void goToGen2DistributionPokemonMenu(void* context, const void*) +{ + goToDistributionPokemonListMenu(context, DistributionPokemonListType::GEN2); +} + +void goToGen2PCNYDistributionPokemonMenu(void* context, const void* param) +{ + goToDistributionPokemonListMenu(context, DistributionPokemonListType::GEN2_POKEMON_CENTER_NEW_YORK); +} + +void gen2ReceiveGSBall(void* context, const void* param) +{ + MenuScene* scene = static_cast(context); + TransferPakManager& tpakManager = scene->getDependencies().tpakManager; + TransferPakRomReader romReader(tpakManager); + TransferPakSaveManager saveManager(tpakManager); + Gen2GameReader gameReader(romReader, saveManager, Gen2GameType::CRYSTAL); + DialogData messageData = {0}; + bool alreadyHasOne = false; + + tpakManager.setRAMEnabled(true); + + const char* trainerName = gameReader.getTrainerName(); + Gen2ItemList keyItemPocket = gameReader.getItemList(Gen2ItemListType::GEN2_ITEMLISTTYPE_KEYITEMPOCKET); + if(keyItemPocket.getCount() > 0) + { + uint8_t itemId; + uint8_t itemCount; + bool gotEntry = keyItemPocket.getEntry(0, itemId, itemCount); + + while(gotEntry) + { + if(itemId == POKEMON_CRYSTAL_ITEM_ID_GS_BALL) + { + alreadyHasOne = true; + break; + } + gotEntry = keyItemPocket.getNextEntry(itemId, itemCount); + } + } + + if(alreadyHasOne) + { + setDialogDataText(messageData, "It appears you already have one!"); + } + else + { + keyItemPocket.add(POKEMON_CRYSTAL_ITEM_ID_GS_BALL, 1); + gameReader.finishSave(); + setDialogDataText(messageData, "%s obtained a GS Ball!", trainerName); + } + scene->showSingleMessage(messageData); + + tpakManager.setRAMEnabled(false); +} \ No newline at end of file diff --git a/src/scenes/AbstractUIScene.cpp b/src/scenes/AbstractUIScene.cpp new file mode 100755 index 0000000..3e56c07 --- /dev/null +++ b/src/scenes/AbstractUIScene.cpp @@ -0,0 +1,97 @@ +#include "scenes/AbstractUIScene.h" +#include "core/DragonUtils.h" +#include "widget/IWidget.h" + +static uint16_t minimumTimeBetweenInputEventsInMs = 150; + +AbstractUIScene::AbstractUIScene(SceneDependencies& deps) + : deps_(deps) + , focusChain_() + , lastInputHandleTime_(0) +{ +} + +AbstractUIScene::~AbstractUIScene() +{ +} + +void AbstractUIScene::processUserInput() +{ + if(!focusChain_) + { + return; + } + + const uint64_t now = get_ticks(); + if(TICKS_TO_MS(now - lastInputHandleTime_) < minimumTimeBetweenInputEventsInMs) + { + // not enough time has passed since last handled input event. Ignore + return; + } + + const joypad_inputs_t inputs = joypad_get_inputs(JOYPAD_PORT_1); + if(handleUserInput(JOYPAD_PORT_1, inputs)) + { + lastInputHandleTime_ = now; + } +} + +bool AbstractUIScene::handleUserInput(joypad_port_t port, const joypad_inputs_t& inputs) +{ + bool ret = focusChain_->current->handleUserInput(inputs); + + if(!ret) + { + // the widget did not handle the userInput. If we're dealing with a navigation key, we may want to switch focus + WidgetFocusChainSegment* nextChainEntry; + const UINavigationKey navKey = determineUINavigationKey(inputs, NavigationInputSourceType::BOTH); + + switch(navKey) + { + case UINavigationKey::UP: + nextChainEntry = focusChain_->onUp; + break; + case UINavigationKey::DOWN: + nextChainEntry = focusChain_->onDown; + break; + case UINavigationKey::LEFT: + nextChainEntry = focusChain_->onLeft; + break; + case UINavigationKey::RIGHT: + nextChainEntry = focusChain_->onRight; + break; + case UINavigationKey::NONE: + default: + nextChainEntry = nullptr; + break; + } + + if(nextChainEntry) + { + focusChain_->current->setFocused(false); + focusChain_ = nextChainEntry; + focusChain_->current->setFocused(true); + ret = true; + } + } + return ret; +} + +void AbstractUIScene::destroy() +{ + setFocusChain(nullptr); +} + +void AbstractUIScene::setFocusChain(WidgetFocusChainSegment* focusChain) +{ + if(focusChain_) + { + focusChain_->current->setFocused(false); + } + focusChain_ = focusChain; + + if(focusChain_) + { + focusChain_->current->setFocused(true); + } +} \ No newline at end of file diff --git a/src/scenes/DistributionPokemonListScene.cpp b/src/scenes/DistributionPokemonListScene.cpp new file mode 100755 index 0000000..8893e8a --- /dev/null +++ b/src/scenes/DistributionPokemonListScene.cpp @@ -0,0 +1,259 @@ +#include "scenes/DistributionPokemonListScene.h" +#include "scenes/SceneManager.h" +#include "transferpak/TransferPakManager.h" + +static uint8_t calculateMainDataChecksum(ISaveManager& saveManager) +{ + Gen1Checksum checksum; + const uint16_t checksummedDataStart = 0x598; + const uint16_t checksummedDataEnd = 0x1523; + const uint16_t numBytes = checksummedDataEnd - checksummedDataStart; + uint16_t i; + uint8_t temp = 0; + uint8_t byte; + + saveManager.seekToBankOffset(1, checksummedDataStart); + + debugf("Checksum - dumping bytes:\r\n"); + for(i=0; i < numBytes; ++i) + { + saveManager.readByte(byte); + debugf(" %02x", byte); + checksum.addByte(byte); + temp += byte; + } + + uint8_t ret = checksum.get(); + debugf("\r\ntemp=%02x, ret=%02x\r\n", temp, ret); + return ret; +} + +static DistributionPokemonListSceneContext* convert(void* context) +{ + return static_cast(context); +} + +static void injectDistributionPokemon(void* context, const void* data) +{ + auto scene = static_cast(context); + scene->triggerPokemonInjection(data); +} + +DistributionPokemonListScene::DistributionPokemonListScene(SceneDependencies& deps, void* context) + : MenuScene(deps, context) + , romReader_(deps.tpakManager) + , saveManager_(deps.tpakManager) + , gen1Reader_(romReader_, saveManager_, static_cast(deps.specificGenVersion)) + , gen2Reader_(romReader_, saveManager_, static_cast(deps.specificGenVersion)) + , diag_() + , pokeToInject_(nullptr) +{ +} + +DistributionPokemonListScene::~DistributionPokemonListScene() +{ +} + +void DistributionPokemonListScene::init() +{ + loadDistributionPokemonList(); + MenuScene::init(); +} + +void DistributionPokemonListScene::destroy() +{ + MenuScene::destroy(); + + delete[] context_->menuEntries; + context_->menuEntries = nullptr; + context_->numMenuEntries = 0; +} + +bool DistributionPokemonListScene::handleUserInput(joypad_port_t port, const joypad_inputs_t& inputs) +{ + if(pokeToInject_) + { + injectPokemon(pokeToInject_); + pokeToInject_ = nullptr; + return true; + } + else + { + return MenuScene::handleUserInput(port, inputs); + } +} + +void DistributionPokemonListScene::triggerPokemonInjection(const void* data) +{ + pokeToInject_ = data; + + setDialogDataText(diag_, "Saving... Don't turn off the power."); + diag_.userAdvanceBlocked = true; + showDialog(&diag_); +} + +void DistributionPokemonListScene::injectPokemon(const void* data) +{ + const Gen1DistributionPokemon* g1Poke; + const Gen2DistributionPokemon* g2Poke; + const char* trainerName; + const char* pokeName; + + deps_.tpakManager.setRAMEnabled(true); + + switch(convert(context_)->listType) + { + case DistributionPokemonListType::GEN1: + g1Poke = static_cast(data); + trainerName = gen1Reader_.getTrainerName(); + pokeName = gen1Reader_.getPokemonName(g1Poke->poke.poke_index); + gen1Reader_.addDistributionPokemon((*g1Poke)); + break; + case DistributionPokemonListType::GEN2: + case DistributionPokemonListType::GEN2_POKEMON_CENTER_NEW_YORK: + g2Poke = static_cast(data); + trainerName = gen2Reader_.getTrainerName(); + pokeName = gen2Reader_.getPokemonName(g2Poke->poke.poke_index); + gen2Reader_.addDistributionPokemon((*g2Poke)); + gen2Reader_.finishSave(); + break; + default: + debugf("%s: ERROR: got DistributionPokemonListType::INVALID! This should never happen!\r\n", __FUNCTION__); + /* 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 + * the Game Boy, the RAM is automatically disabled to protect it." + * + * source: https://gbdev.io/pandocs/MBC1.html + * + * Yes, I'm aware we're dealing with MBC3 here, but there's some overlap and what applies to MBC1 here likely also applies + * to MBC3 + */ + deps_.tpakManager.setRAMEnabled(false); + return; + } + + deps_.tpakManager.finishWrites(); + + calculateMainDataChecksum(saveManager_); + + // The reason is the same as previous setRAMEnabled(false) statement above + deps_.tpakManager.setRAMEnabled(false); + + // operation done. Now the dialog can be advanced and we can show confirmation that the user got the pokémon + dialogWidget_.advanceDialog(); + + setDialogDataText(diag_, "%s received %s!", trainerName, pokeName); + diag_.userAdvanceBlocked = false; + showDialog(&diag_); +} + +void DistributionPokemonListScene::onDialogDone() +{ + if(diag_.userAdvanceBlocked) + { + // ignore this notification. We advanced this one ourselves to get to the next one + return; + } + // We're done with the injection. Go back to the previous menu + deps_.sceneManager.goBackToPreviousScene(); +} + +void DistributionPokemonListScene::setupMenu() +{ + const VerticalListStyle listStyle = { + .backgroundSprite = menu9SliceSprite_, + .backgroundSpriteSettings = { + .renderMode = SpriteRenderMode::NINESLICE, + .srcRect = { 6, 6, 6, 6 } + }, + .marginTop = 5, + }; + + menuList_.setStyle(listStyle); + menuList_.setBounds(Rectangle{20, 20, 280, 150}); + menuList_.setVisible(true); + + cursorWidget_.setVisible(false); + + const MenuItemStyle itemStyle = { + .size = {280, 16}, + .titleNotFocused = { + .fontId = arialId_, + .fontStyleId = fontStyleWhiteId_ + }, + .titleFocused = { + .fontId = arialId_, + .fontStyleId = fontStyleYellowId_ + }, + .leftMargin = 10, + .topMargin = 1 + }; + + menuListFiller_.addItems(context_->menuEntries, context_->numMenuEntries, itemStyle); +} + +void DistributionPokemonListScene::loadDistributionPokemonList() +{ + const Gen1DistributionPokemon** gen1List; + const Gen2DistributionPokemon** gen2List; + uint32_t listSize; + uint32_t i; + + DistributionPokemonListSceneContext* context = convert(context_); + + switch(context->listType) + { + case DistributionPokemonListType::GEN1: + gen1_getMainDistributionPokemonList(gen1List, listSize); + gen2List = nullptr; + break; + case DistributionPokemonListType::GEN2: + gen1List = nullptr; + gen2_getMainDistributionPokemonList(gen2List, listSize); + break; + case DistributionPokemonListType::GEN2_POKEMON_CENTER_NEW_YORK: + gen1List = nullptr; + gen2_getPokemonCenterNewYorkDistributionPokemonList(gen2List, listSize); + break; + default: + gen1List = nullptr; + gen2List = nullptr; + listSize = 0; + break; + } + + if(!listSize) + { + return; + } + context->menuEntries = new MenuItemData[listSize]; + context->numMenuEntries = listSize; + + if(gen1List) + { + for(i = 0; i < listSize; ++i) + { + context->menuEntries[i].title = gen1List[i]->name; + context->menuEntries[i].onConfirmAction = injectDistributionPokemon; + context->menuEntries[i].context = this; + context->menuEntries[i].itemParam = gen1List[i]; + } + } + else if(gen2List) + { + for(i = 0; i < listSize; ++i) + { + context->menuEntries[i].title = gen2List[i]->name; + context->menuEntries[i].onConfirmAction = injectDistributionPokemon; + context->menuEntries[i].context = this; + context->menuEntries[i].itemParam = gen2List[i]; + } + } +} + +void deleteDistributionPokemonListSceneContext(void* context) +{ + auto toDelete = static_cast(context); + delete toDelete; +} \ No newline at end of file diff --git a/src/scenes/IScene.cpp b/src/scenes/IScene.cpp new file mode 100755 index 0000000..7a18e02 --- /dev/null +++ b/src/scenes/IScene.cpp @@ -0,0 +1,5 @@ +#include "scenes/IScene.h" + +IScene::~IScene() +{ +} \ No newline at end of file diff --git a/src/scenes/InitTransferPakScene.cpp b/src/scenes/InitTransferPakScene.cpp new file mode 100755 index 0000000..e6c8aff --- /dev/null +++ b/src/scenes/InitTransferPakScene.cpp @@ -0,0 +1,221 @@ +#include "scenes/InitTransferPakScene.h" +#include "scenes/MenuScene.h" +#include "scenes/SceneManager.h" +#include "transferpak/TransferPakManager.h" +#include "transferpak/TransferPakRomReader.h" +#include "transferpak/TransferPakSaveManager.h" +#include "gen1/Gen1GameReader.h" +#include "gen2/Gen2GameReader.h" +#include "menu/MenuEntries.h" + +static void dialogFinishedCallback(void* context) +{ + InitTransferPakScene* scene = (InitTransferPakScene*)context; + scene->onDialogDone(); +} + +static void tpakWidgetStateChangedCallback(void* context, TransferPakWidgetState newState) +{ + InitTransferPakScene* scene = (InitTransferPakScene*)context; + scene->onTransferPakWidgetStateChanged(newState); +} + +InitTransferPakScene::InitTransferPakScene(SceneDependencies& deps, void*) + : SceneWithDialogWidget(deps) + , menu9SliceSprite_(nullptr) + , tpakDetectWidget_(deps.animationManager, deps.tpakManager) + , tpakDetectWidgetSegment_(WidgetFocusChainSegment{ + .current = &tpakDetectWidget_ + }) + , diagData_({0}) + , playerName_() + , gameTypeString_(nullptr) +{ + playerName_[0] = '\0'; + playerName_[PLAYER_NAME_SIZE - 1] = '\0'; +} + +InitTransferPakScene::~InitTransferPakScene() +{ +} + +void InitTransferPakScene::init() +{ + menu9SliceSprite_ = sprite_load("rom://menu-bg-9slice.sprite"); + + SceneWithDialogWidget::init(); + + setupTPakDetectWidget(); + setFocusChain(&tpakDetectWidgetSegment_); +} + +void InitTransferPakScene::destroy() +{ + SceneWithDialogWidget::destroy(); + + sprite_free(menu9SliceSprite_); + menu9SliceSprite_ = nullptr; +} + +void InitTransferPakScene::render(RDPQGraphics& gfx, const Rectangle& sceneBounds) +{ + //gfx.fillRectangle(Rectangle{.x = 0, .y = 0, .width = 100, .height = 100}, RGBA16(31, 0, 0, 1)); + tpakDetectWidget_.render(gfx, sceneBounds); + + SceneWithDialogWidget::render(gfx, sceneBounds); +} + +void InitTransferPakScene::onDialogDone() +{ + MenuSceneContext* menuContext; + Gen1GameType gen1Type; + Gen2GameType gen2Type; + + tpakDetectWidget_.retrieveGameType(gen1Type, gen2Type); + + if(gen1Type != Gen1GameType::INVALID) + { + menuContext = new MenuSceneContext({ + .menuEntries = gen1MenuEntries, + .numMenuEntries = static_cast(gen1MenuEntriesSize / sizeof(gen1MenuEntries[0])) + }); + } + else if(gen2Type != Gen2GameType::INVALID) + { + if(gen2Type == Gen2GameType::CRYSTAL) + { + menuContext = new MenuSceneContext({ + .menuEntries = gen2CrystalMenuEntries, + .numMenuEntries = static_cast(gen2CrystalMenuEntriesSize / sizeof(gen2CrystalMenuEntries[0])) + }); + } + else + { + menuContext = new MenuSceneContext({ + .menuEntries = gen2MenuEntries, + .numMenuEntries = static_cast(gen2MenuEntriesSize / sizeof(gen2MenuEntries[0])) + }); + } + } + else + { + debugf("ERROR: Not gen 1 nor gen 2. This shouldn't be happening!\r\n"); + return; + } + + deps_.sceneManager.switchScene(SceneType::MENU, deleteMenuSceneContext, menuContext); +} + +void InitTransferPakScene::onTransferPakWidgetStateChanged(TransferPakWidgetState newState) +{ + debugf("onTransferPakWidgetStateChanged(%d)\r\n", static_cast(newState)); + if(newState == TransferPakWidgetState::GAME_FOUND) + { + debugf("[InitTransferPakScene]: Game found!\r\n"); + deps_.tpakManager.setRAMEnabled(true); + loadGameMetadata(); + /* 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 + * the Game Boy, the RAM is automatically disabled to protect it." + * + * source: https://gbdev.io/pandocs/MBC1.html + * + * Yes, I'm aware we're dealing with MBC3 here, but there's some overlap and what applies to MBC1 here likely also applies + * to MBC3 + */ + deps_.tpakManager.setRAMEnabled(false); + + setDialogDataText(diagData_, "Hi %s! We've detected Pokémon %s in the N64 Transfer Pak. Let's go!", playerName_, gameTypeString_); + dialogWidget_.appendDialogData(&diagData_); + dialogWidget_.setVisible(true); + setFocusChain(&dialogFocusChainSegment_); + } +} + +void InitTransferPakScene::setupTPakDetectWidget() +{ + const TransferPakDetectionWidgetStyle style = { + .textSettings = { + .fontId = arialId_, + .fontStyleId = fontStyleWhiteId_ + } + }; + + tpakDetectWidget_.setStyle(style); + tpakDetectWidget_.setStateChangedCallback(tpakWidgetStateChangedCallback, this); + tpakDetectWidget_.setBounds(Rectangle{60, 90, 200, 60}); +} + +void InitTransferPakScene::setupDialog(DialogWidgetStyle& style) +{ + style.backgroundSprite = menu9SliceSprite_; + style.backgroundSpriteSettings = { + .renderMode = SpriteRenderMode::NINESLICE, + .srcRect = { 6, 6, 6, 6 } + }; + + SceneWithDialogWidget::setupDialog(style); + + dialogWidget_.setOnDialogFinishedCallback(dialogFinishedCallback, this); + dialogWidget_.setVisible(false); +} + +void InitTransferPakScene::loadGameMetadata() +{ + 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); + + switch(gen1Type) + { + case Gen1GameType::BLUE: + gameTypeString_ = "Blue"; + break; + case Gen1GameType::RED: + gameTypeString_ = "Red"; + break; + case Gen1GameType::YELLOW: + gameTypeString_ = "Yellow"; + break; + default: + gameTypeString_ = ""; + break; + } + } + 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); + + switch(gen2Type) + { + case Gen2GameType::GOLD: + gameTypeString_ = "Gold"; + break; + case Gen2GameType::SILVER: + gameTypeString_ = "Silver"; + break; + case Gen2GameType::CRYSTAL: + gameTypeString_ = "Crystal"; + break; + default: + gameTypeString_ = ""; + break; + } + } + playerName_[PLAYER_NAME_SIZE - 1] = '\0'; +} diff --git a/src/scenes/MenuScene.cpp b/src/scenes/MenuScene.cpp new file mode 100755 index 0000000..4b8b706 --- /dev/null +++ b/src/scenes/MenuScene.cpp @@ -0,0 +1,229 @@ +#include "scenes/MenuScene.h" +#include "core/FontManager.h" +#include "scenes/SceneManager.h" + +#include + +static void dialogFinishedCallback(void* context) +{ + MenuScene* scene = (MenuScene*)context; + scene->onDialogDone(); +} + +MenuScene::MenuScene(SceneDependencies& deps, void* context) + : SceneWithDialogWidget(deps) + , context_(static_cast(context)) + , menu9SliceSprite_(nullptr) + , cursorSprite_(nullptr) + , menuList_(deps.animationManager) + , cursorWidget_(deps.animationManager) + , menuListFiller_(menuList_) + , listFocusChainSegment_(WidgetFocusChainSegment{ + .current = &menuList_ + }) + , fontStyleYellowId_(1) + , bButtonPressed_(false) + , singleMessageDialog_({0}) +{ +} + +MenuScene::~MenuScene() +{ + // we registered ourselves as context before. (setupMenu) + // this instance is about to become delete'd + // we need to reset every context referring to this instance to prevent + // crashes the next time we load the same menuentries + for(unsigned i=0; i < context_->numMenuEntries; ++i) + { + if(context_->menuEntries[i].context == this) + { + context_->menuEntries[i].context = nullptr; + } + } +} + +void MenuScene::init() +{ + // load these sprites before the parent init because setupDialog(style) will need them + menu9SliceSprite_ = sprite_load("rom://menu-bg-9slice.sprite"); + cursorSprite_ = sprite_load("rom://hand-cursor.sprite"); + + SceneWithDialogWidget::init(); + + setupMenu(); + + setFocusChain(&listFocusChainSegment_); +} + +void MenuScene::destroy() +{ + menuList_.unregisterFocusListener(this); + menuList_.clearWidgets(); + menuList_.setStyle({0}); + cursorWidget_.setStyle({0}); + + // destroy the parent before releasing the sprites because the dialog widget + // may still have a reference to them + SceneWithDialogWidget::destroy(); + + sprite_free(cursorSprite_); + sprite_free(menu9SliceSprite_); +} + +void MenuScene::render(RDPQGraphics& gfx, const Rectangle& sceneBounds) +{ + menuList_.render(gfx, sceneBounds); + cursorWidget_.render(gfx, sceneBounds); + SceneWithDialogWidget::render(gfx, sceneBounds); + + TextRenderSettings renderSettings = { + .fontId = arialId_, + .fontStyleId = fontStyleWhiteId_ + }; + gfx.drawText(Rectangle{40, 10, 280, 16}, "PokeMe64 by risingPhil. Very early version...", renderSettings); +} + +bool MenuScene::handleUserInput(joypad_port_t port, const joypad_inputs_t& inputs) +{ + if(SceneWithDialogWidget::handleUserInput(port, inputs)) + { + return true; + } + + if(inputs.btn.b) + { + // we will only handle b button release. + bButtonPressed_ = true; + return true; + } + else if(bButtonPressed_) + { + // b button release occurred. Switch back to previous scene. + bButtonPressed_ = false; + deps_.sceneManager.goBackToPreviousScene(); + return true; + } + return false; +} + +void MenuScene::onDialogDone() +{ + dialogWidget_.setVisible(false); + menuList_.setVisible(true); + cursorWidget_.setVisible(true); + setFocusChain(&listFocusChainSegment_); +} + +void MenuScene::focusChanged(const FocusChangeStatus& status) +{ + const Rectangle newCursorBounds = { + .x = status.focusBounds.x + 2, + .y = status.focusBounds.y, + .width = cursorSprite_->width, + .height = cursorSprite_->height + }; + + cursorWidget_.moveToBounds(newCursorBounds); +} + +SceneDependencies& MenuScene::getDependencies() +{ + return deps_; +} + +void MenuScene::showSingleMessage(const DialogData& messageData) +{ + singleMessageDialog_ = messageData; + showDialog(&singleMessageDialog_); +} + +void MenuScene::setupFonts() +{ + SceneWithDialogWidget::setupFonts(); + + const rdpq_fontstyle_t arialYellow = { + .color = RGBA32(0xFF, 0xFF, 0x00, 0xFF), + .outline_color = RGBA32(0, 0, 0, 0xFF) + }; + + deps_.fontManager.registerFontStyle(arialId_, fontStyleYellowId_, arialYellow); +} + +void MenuScene::setupMenu() +{ + const VerticalListStyle listStyle = { + .backgroundSprite = menu9SliceSprite_, + .backgroundSpriteSettings = { + .renderMode = SpriteRenderMode::NINESLICE, + .srcRect = { 6, 6, 6, 6 } + }, + .marginTop = 5 + }; + + const CursorStyle cursorStyle = { + .sprite = cursorSprite_, + .idleMoveDiff = { 5, 0, 0, 0 }, + .idleAnimationDurationInMs = 500, + .moveAnimationDurationInMs = 250 + }; + + menuList_.setStyle(listStyle); + menuList_.setBounds(Rectangle{100, 30, 150, 150}); + menuList_.setVisible(true); + cursorWidget_.setStyle(cursorStyle); + cursorWidget_.setVisible(true); + menuList_.registerFocusListener(this); + + const MenuItemStyle itemStyle = { + .size = {150, 16}, + .titleNotFocused = { + .fontId = arialId_, + .fontStyleId = fontStyleWhiteId_ + }, + .titleFocused = { + .fontId = arialId_, + .fontStyleId = fontStyleYellowId_ + }, + .leftMargin = 35, + .topMargin = 1 + }; + + for(unsigned i=0; i < context_->numMenuEntries; ++i) + { + if(!context_->menuEntries[i].context) + { + context_->menuEntries[i].context = this; + } + } + + menuListFiller_.addItems(context_->menuEntries, context_->numMenuEntries, itemStyle); +} + +void MenuScene::setupDialog(DialogWidgetStyle& style) +{ + style.backgroundSprite = menu9SliceSprite_; + style.backgroundSpriteSettings = { + .renderMode = SpriteRenderMode::NINESLICE, + .srcRect = { 6, 6, 6, 6 } + }; + + SceneWithDialogWidget::setupDialog(style); + + dialogWidget_.setOnDialogFinishedCallback(dialogFinishedCallback, this); + dialogWidget_.setVisible(false); +} + +void MenuScene::showDialog(DialogData* diagData) +{ + menuList_.setVisible(false); + cursorWidget_.setVisible(false); + dialogWidget_.setVisible(true); + dialogWidget_.setData(diagData); + setFocusChain(&dialogFocusChainSegment_); +} + +void deleteMenuSceneContext(void* context) +{ + MenuSceneContext* menuContext = static_cast(context); + delete menuContext; +} \ No newline at end of file diff --git a/src/scenes/SceneManager.cpp b/src/scenes/SceneManager.cpp new file mode 100755 index 0000000..1d6aadd --- /dev/null +++ b/src/scenes/SceneManager.cpp @@ -0,0 +1,144 @@ +#include "scenes/SceneManager.h" +#include "scenes/TestScene.h" +#include "scenes/InitTransferPakScene.h" +#include "scenes/DistributionPokemonListScene.h" + +#include + +SceneManager::SceneManager(RDPQGraphics& gfx, AnimationManager& animationManager, FontManager& fontManager, TransferPakManager& tpakManager) + : sceneHistory_() + , sceneDeps_(SceneDependencies{ + .gfx = gfx, + .animationManager = animationManager, + .fontManager = fontManager, + .tpakManager = tpakManager, + .sceneManager = (*this), + .generation = 0, + .specificGenVersion = 0 + }) + , scene_(nullptr) + , newSceneType_(SceneType::NONE) + , newSceneContext_(nullptr) + , contextToDelete_(nullptr) + , deleteContextFunc_(nullptr) +{ +} + +SceneManager::~SceneManager() +{ + unloadScene(scene_); +} + +void SceneManager::switchScene(SceneType type, void (*deleteContextFunc)(void*), void* sceneContext) +{ + newSceneType_ = type; + newSceneContext_ = sceneContext; + + sceneHistory_.push_back(SceneHistorySegment{ + .type = type, + .context = sceneContext, + .deleteContextFunc = deleteContextFunc + }); +} + +void SceneManager::goBackToPreviousScene() +{ + { + SceneHistorySegment& curEntry = sceneHistory_.back(); + contextToDelete_ = curEntry.context; + deleteContextFunc_ = curEntry.deleteContextFunc; + } + sceneHistory_.pop_back(); + + { + SceneHistorySegment& lastEntry = sceneHistory_.back(); + newSceneType_ = lastEntry.type; + newSceneContext_ = lastEntry.context; + } +} + +void SceneManager::clearHistory() +{ + for(SceneHistorySegment& entry : sceneHistory_) + { + if(entry.context) + { + entry.deleteContextFunc(entry.context); + } + } + sceneHistory_.clear(); +} + +void SceneManager::handleUserInput() +{ + if(!scene_) + { + return; + } + scene_->processUserInput(); +} + +void SceneManager::render(const Rectangle& sceneBounds) +{ + if(newSceneType_ != SceneType::NONE) + { + loadScene(); + } + if(!scene_) + { + return; + } + + scene_->render(sceneDeps_.gfx, sceneBounds); +} + +void SceneManager::loadScene() +{ + IScene* oldScene = scene_; + + switch(newSceneType_) + { + case SceneType::INIT_TRANSFERPAK: + scene_ = new InitTransferPakScene(sceneDeps_, newSceneContext_); + break; + case SceneType::MENU: + scene_ = new MenuScene(sceneDeps_, newSceneContext_); + break; + case SceneType::DISTRIBUTION_POKEMON_LIST: + scene_ = new DistributionPokemonListScene(sceneDeps_, newSceneContext_); + break; + case SceneType::TEST: + scene_ = new TestScene(sceneDeps_, newSceneContext_); + break; + default: + break; + } + + if(!scene_) + { + scene_ = oldScene; + return; + } + unloadScene(oldScene); + + if(contextToDelete_) + { + deleteContextFunc_(contextToDelete_); + contextToDelete_ = nullptr; + deleteContextFunc_ = nullptr; + } + + scene_->init(); + newSceneType_ = SceneType::NONE; +} + +void SceneManager::unloadScene(IScene* scene) +{ + if(!scene) + { + return; + } + + scene->destroy(); + delete scene; +} \ No newline at end of file diff --git a/src/scenes/SceneWithDialogWidget.cpp b/src/scenes/SceneWithDialogWidget.cpp new file mode 100755 index 0000000..7217f2c --- /dev/null +++ b/src/scenes/SceneWithDialogWidget.cpp @@ -0,0 +1,67 @@ +#include "scenes/SceneWithDialogWidget.h" +#include "scenes/SceneManager.h" +#include "core/FontManager.h" + +SceneWithDialogWidget::SceneWithDialogWidget(SceneDependencies& deps) + : AbstractUIScene(deps) + , dialogWidget_(deps.animationManager) + , dialogFocusChainSegment_({ + .current = &dialogWidget_ + }) + , arialId_(1) + , fontStyleWhiteId_(0) +{ +} + +SceneWithDialogWidget::~SceneWithDialogWidget() +{ +} + +void SceneWithDialogWidget::init() +{ + DialogWidgetStyle style = { + .textSettings = { + .fontId = arialId_, + .fontStyleId = fontStyleWhiteId_ + }, + .marginLeft = 10, + .marginRight = 10, + .marginTop = 10, + .marginBottom = 10 + }; + + setupFonts(); + setupDialog(style); + + setFocusChain(&dialogFocusChainSegment_); +} + +void SceneWithDialogWidget::destroy() +{ + dialogWidget_.setData(nullptr); + dialogWidget_.setStyle({0}); + + AbstractUIScene::destroy(); +} + +void SceneWithDialogWidget::render(RDPQGraphics& gfx, const Rectangle& sceneBounds) +{ + dialogWidget_.render(gfx, sceneBounds); +} + +void SceneWithDialogWidget::setupFonts() +{ + arialId_ = deps_.fontManager.getFont("rom://Arial.font64"); + const rdpq_fontstyle_t arialWhite = { + .color = RGBA32(0xFF, 0xFF, 0xFF, 0xFF), + .outline_color = RGBA32(0, 0, 0, 0xFF) + }; + + deps_.fontManager.registerFontStyle(arialId_, fontStyleWhiteId_, arialWhite); +} + +void SceneWithDialogWidget::setupDialog(DialogWidgetStyle& style) +{ + dialogWidget_.setStyle(style); + dialogWidget_.setBounds(Rectangle{10, 180, 300, 50}); +} \ No newline at end of file diff --git a/src/scenes/TestScene.cpp b/src/scenes/TestScene.cpp new file mode 100755 index 0000000..89fffd0 --- /dev/null +++ b/src/scenes/TestScene.cpp @@ -0,0 +1,93 @@ +#include "scenes/TestScene.h" +#include "core/RDPQGraphics.h" + +#include +#include + +static const char* tvtypeToString(tv_type_t type) +{ + switch(type) + { + case TV_PAL: + return "PAL"; + case TV_NTSC: + return "NTSC"; + case TV_MPAL: + return "MPAL"; + default: + return "INVALID"; + } +} + +TestScene::TestScene(SceneDependencies& deps, void*) + : AbstractUIScene(deps) + , arialFont_(nullptr) + , arialFontId_(1) + , fontStyleWhite_(0) + , pokeballSprite_(nullptr) + , oakSprite_(nullptr) + , menu9SliceSprite_(nullptr) + , rectBounds_({0, 0, 320, 240}) + , textRect_({70, 70, 180, 60}) + , spriteBounds_({320 - 128, 0, 128, 128}) + , oakBounds_({0}) + , oakSrcBounds_({0}) + , menuBounds_({ 100, 20, 100, 100}) + , textRenderSettings_({}) + , menuRenderSettings_({ + .renderMode = SpriteRenderMode::NINESLICE, + .srcRect = { 6, 6, 6, 6 } + }) +{ +} + +TestScene::~TestScene() +{ +} + +void TestScene::init() +{ + arialFont_ = rdpq_font_load("rom://Arial.font64"); + rdpq_fontstyle_t arialWhiteFontStyle = { + .color = RGBA32(0xFF, 0xFF, 0xFF, 0xFF) + }; + rdpq_font_style(arialFont_, fontStyleWhite_, &arialWhiteFontStyle); + + // TODO: this is a problem: there's no way to unregister a font. + // Therefore if we'd load the same font > 1 times, we'll get a crash (due to assert) + // We'll need to create a FontManager class to handle this + rdpq_text_register_font(arialFontId_, arialFont_); + textRenderSettings_.fontId = arialFontId_; + + pokeballSprite_ = sprite_load("rom:/pokeball.sprite"); + oakSprite_ = sprite_load("rom://oak.sprite"); + menu9SliceSprite_ = sprite_load("rom://menu-bg-9slice.sprite"); + + oakBounds_ ={10, 80, oakSprite_->width, oakSprite_->height}; + oakSrcBounds_ = {oakSprite_->width / 3, oakSprite_->height / 3, oakSprite_->width * 2 / 3, oakSprite_->height * 2 / 3}; + + printf("Hello Phil! Your tv type is: %s\n", tvtypeToString(get_tv_type())); +} + +void TestScene::destroy() +{ + sprite_free(menu9SliceSprite_); + menu9SliceSprite_ = nullptr; + + sprite_free(oakSprite_); + oakSprite_ = nullptr; + + sprite_free(pokeballSprite_); + pokeballSprite_ = nullptr; + + rdpq_font_free(arialFont_); +} + +void TestScene::render(RDPQGraphics& gfx, const Rectangle& /*sceneBounds*/) +{ +// gfx.fillRectangle(rectBounds_, RGBA32(200, 0, 0, 0)); +// gfx.drawText(textRect_, "Hello Phil!", textRenderSettings_); +// gfx.drawSprite(spriteBounds_, pokeballSprite_, SpriteRenderSettings()); +// gfx.drawSprite(oakBounds_, oakSprite_, {.srcRect = oakSrcBounds_}); + gfx.drawSprite(menuBounds_, menu9SliceSprite_, menuRenderSettings_); +} \ No newline at end of file diff --git a/src/transferpak/TransferPakManager.cpp b/src/transferpak/TransferPakManager.cpp new file mode 100755 index 0000000..f910add --- /dev/null +++ b/src/transferpak/TransferPakManager.cpp @@ -0,0 +1,299 @@ +#include "transferpak/TransferPakManager.h" + +#include +#include + +/** @brief Transfer Pak address for cartridge data */ +#define TPAK_ADDRESS_DATA 0xC000 + +static const uint16_t sramBankStartGBAddress = 0xA000; + +TransferPakManager::TransferPakManager() + : port_(JOYPAD_PORT_1) + , wasPoweredAtLeastOnce_(false) + , currentSRAMBank_(0) + , readBufferBankOffset_(0xFFFF) + , writeBufferSRAMBankOffset_(0xFFFF) + , readBuffer_() + , writeBuffer_() +{ +} + +TransferPakManager::~TransferPakManager() +{ +} + +joypad_port_t TransferPakManager::getPort() const +{ + return port_; +} + +void TransferPakManager::setPort(joypad_port_t port) +{ + port_ = port; + wasPoweredAtLeastOnce_ = false; +} + +bool TransferPakManager::hasTransferPak() +{ + if(!joypad_is_connected(port_)) + { + debugf("[TransferPakManager]: joypad not connected %d\r\n", (int)port_); + return false; + } + + const joypad_accessory_type_t type = joypad_get_accessory_type(port_); + debugf("[TransferPakManager]: accessory type %d\r\n", (int)type); + return (type == JOYPAD_ACCESSORY_TYPE_TRANSFER_PAK); +} + +bool TransferPakManager::setPower(bool on) +{ + int ret; + + if(!wasPoweredAtLeastOnce_) + { + if(!on) + { + return true; + } + ret = tpak_init(static_cast(port_)); + if(ret) + { + debugf("[TransferPakManager]: %s: tpak_init got error %d\r\n", __FUNCTION__, ret); + } + wasPoweredAtLeastOnce_ = true; + } + else + { + ret = tpak_set_power(port_, on); + if(ret) + { + debugf("[TransferPakManager]: %s: tpak_set_power got error %d\r\n", __FUNCTION__, ret); + } + } + return (!ret); +} + +uint8_t TransferPakManager::getStatus() +{ + return tpak_get_status(static_cast(port_)); +} + +bool TransferPakManager::validateGbHeader() +{ + gameboy_cartridge_header header; + uint8_t status = getStatus(); + int ret; + bool retBool; + + while(!(status | TPAK_STATUS_READY)) + { + debugf("[TransferPakManager]: ERROR: transfer pak not ready yet. Current status is %hu\r\n", status); + status = getStatus(); + } + + if(status == TPAK_STATUS_REMOVED) + { + debugf("[TransferPakManager]: ERROR: transfer pak has STATUS_REMOVED\r\n"); + return false; + } + + ret = tpak_get_cartridge_header(static_cast(port_), &header); + 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; +} + +void TransferPakManager::switchGBROMBank(uint8_t bankIndex) +{ + uint8_t data[TPAK_BLOCK_SIZE]; + +// debugf("[TransferPakManager]: %s(%hu)\r\n", __FUNCTION__, bankIndex); + + memset(data, bankIndex, TPAK_BLOCK_SIZE); + tpak_write(port_, 0x2000, data, TPAK_BLOCK_SIZE); + + // invalidate read buffer + readBufferBankOffset_ = 0xFFFF; +} + +void TransferPakManager::setRAMEnabled(bool enabled) +{ + uint8_t data[TPAK_BLOCK_SIZE]; + +// debugf("[TransferPakManager]: %s(%d)\r\n", __FUNCTION__, enabled); + + const uint8_t valueToWrite = (enabled) ? 0xA : 0x0; + memset(data, valueToWrite, TPAK_BLOCK_SIZE); + + tpak_write(port_, 0x0, data, TPAK_BLOCK_SIZE); + + uint8_t status = getStatus(); + + while(!(status | TPAK_STATUS_READY)) + { + debugf("[TransferPakManager]: %s: ERROR: transfer pak not ready yet. Current status is %hu\r\n", __FUNCTION__, status); + status = getStatus(); + } +} + +void TransferPakManager::switchGBSRAMBank(uint8_t bankIndex) +{ + uint8_t data[TPAK_BLOCK_SIZE]; + + if(bankIndex == currentSRAMBank_) + { + return; + } +// debugf("[TransferPakManager]: %s(%hu)\r\n", __FUNCTION__, bankIndex); + // make sure to finish any writes in the write buffer before switching + finishWrites(); + + memset(data, bankIndex, TPAK_BLOCK_SIZE); + tpak_write(port_, 0x4000, data, TPAK_BLOCK_SIZE); + + currentSRAMBank_ = bankIndex; + // invalidate read and write buffer + readBufferBankOffset_ = 0xFFFF; + writeBufferSRAMBankOffset_ = 0xFFFF; +} + +void TransferPakManager::read(uint16_t gbAddress, uint8_t* data, uint16_t size) +{ +// debugf("[TransferPakManager]: %s(0x%x, %p, %u)\r\n", __FUNCTION__, gbAddress, data, size); + + uint16_t bytesRemaining = size; + uint8_t* cur = data; + + // we need to do 32-byte aligned reads + uint16_t readBufOffset = gbAddress % TPAK_BLOCK_SIZE; + uint16_t alignedGbAddress = gbAddress - readBufOffset; + uint16_t currentReadSize; + + // first of all determine if we already have a filled readBuffer around this address + if(readBufferBankOffset_ == 0xFFFF || (alignedGbAddress < readBufferBankOffset_) || (alignedGbAddress >= readBufferBankOffset_ + TPAK_BLOCK_SIZE)) + { + // the readBuffer doesn't contain the data we're looking for + // so read some + readBufferBankOffset_ = alignedGbAddress; + // we need to read into the readBuffer first +// debugf("[TransferPakManager]: %s -> tpak_read(%d, 0x%x, %p, %u)\r\n", __FUNCTION__, port_, readBufferBankOffset_, readBuffer_, TPAK_BLOCK_SIZE); + tpak_read(port_, readBufferBankOffset_, readBuffer_, TPAK_BLOCK_SIZE); + } + + + while(bytesRemaining > 0) + { + // copy from the read buffer + currentReadSize = std::min(bytesRemaining, TPAK_BLOCK_SIZE - readBufOffset); + memcpy(cur, readBuffer_ + readBufOffset, currentReadSize); + + bytesRemaining -= currentReadSize; + cur += currentReadSize; + + // check if we still need more bytes after this. If so, we've reached the end of our read buffer + // and will need to read more from the transfer pak + if(bytesRemaining > 0) + { + // we have reached the end of our readBuffer_ + // we need to read more from the transfer pak + readBufferBankOffset_ += TPAK_BLOCK_SIZE; + readBufOffset = 0; +// debugf("[TransferPakManager]: %s -> tpak_read(%d, 0x%x, %p, %u)\r\n", __FUNCTION__, port_, readBufferBankOffset_, readBuffer_, TPAK_BLOCK_SIZE); + tpak_read(port_, readBufferBankOffset_, readBuffer_, TPAK_BLOCK_SIZE); + } + } +} + +void TransferPakManager::readSRAM(uint16_t SRAMBankOffset, uint8_t* data, uint16_t size) +{ +// debugf("[TransferPakManager]: %s(0x%hx, %p, %hu)\r\n", __FUNCTION__, SRAMBankOffset, data, size); + + // make sure to finish any writes before reading. Otherwise we might be reading outdated SRAM data + // after all: we might have pending changes into our writebuffer and therefore we must be sure these are applied first. + finishWrites(); + + read(sramBankStartGBAddress + SRAMBankOffset, data, size); +} + +void TransferPakManager::writeSRAM(uint16_t SRAMBankOffset, const uint8_t* data, uint16_t size) +{ + uint16_t bytesRemaining = size; + uint16_t writeBufferOffset = SRAMBankOffset % TPAK_BLOCK_SIZE; + const uint16_t alignedSRAMBankOffset = SRAMBankOffset - writeBufferOffset; + uint16_t currentWriteSize; + const uint8_t* cur = data; + +// debugf("[TransferPakManager]: %s(0x%hx, %p, %hu)\r\n", __FUNCTION__, SRAMBankOffset, data, size); + + // check if our current SRAM location is outside of the write buffer. If it is, we need to actually start writing the pending changes in the writeBuffer_ before continueing + if((alignedSRAMBankOffset < writeBufferSRAMBankOffset_) || (alignedSRAMBankOffset >= (writeBufferSRAMBankOffset_ + TPAK_BLOCK_SIZE))) + { + // the new write is outside the boundaries of the previous write block + // actually write the writeBuffer_ to SRAM now + finishWrites(); + } + + if(writeBufferSRAMBankOffset_ == 0xFFFF) + { + // we don't have any pending writes + // before doing a write to the write buffer, we need to read the current data first +// debugf("[TransferPakManager]: initial read write buffer offset 0x%02hx\r\n", writeBufferSRAMBankOffset_); + readSRAM(alignedSRAMBankOffset, writeBuffer_, TPAK_BLOCK_SIZE); + // don't modify writeBufferSRAMBankOffset_ before calling readSRAM, otherwise finishWrites() will write whatever + // is in the writeBuffer because writeBufferSRAMBankOffset_ wouldn't be 0xFFFF anymore + writeBufferSRAMBankOffset_ = alignedSRAMBankOffset; + } + + while(bytesRemaining > 0) + { + currentWriteSize = std::min(bytesRemaining, TPAK_BLOCK_SIZE - writeBufferOffset); + memcpy(writeBuffer_ + writeBufferOffset, cur, currentWriteSize); + + cur += currentWriteSize; + bytesRemaining -= currentWriteSize; + + // check if we still have more data to write. + // if so, the current writeBuffer end has been reached and we need to actually write it and continue + if(bytesRemaining > 0) + { + // read the current data of the new block first, because we must write in blocks of 32 bytes + // this will also call finishWrites() + const uint16_t newOffset = writeBufferSRAMBankOffset_ + TPAK_BLOCK_SIZE; +// debugf("[TransferPakManager]: read new write buffer offset 0x%hx\r\n", newOffset); + readSRAM(newOffset, writeBuffer_, TPAK_BLOCK_SIZE); + // don't modify writeBufferSRAMBankOffset before readSRAM, otherwise finishWrites() in readSRAM would + // write to the wrong offset + writeBufferSRAMBankOffset_ = newOffset; + writeBufferOffset = 0; + } + } +} + +void TransferPakManager::finishWrites() +{ + if(writeBufferSRAMBankOffset_ == 0xFFFF) + { + // no pending writes + return; + } +// debugf("[TransferPakManager]: %s writeBufferSRAMOffset 0x%hx, buffer %p, blocksize %hu\r\n", __FUNCTION__, writeBufferSRAMBankOffset_, writeBuffer_, TPAK_BLOCK_SIZE); + + tpak_write(port_, sramBankStartGBAddress + writeBufferSRAMBankOffset_, writeBuffer_, TPAK_BLOCK_SIZE); + + // mark no pending writes + writeBufferSRAMBankOffset_ = 0xFFFF; + // also invalidate read buffer + readBufferBankOffset_ = 0xFFFF; +} \ No newline at end of file diff --git a/src/transferpak/TransferPakRomReader.cpp b/src/transferpak/TransferPakRomReader.cpp new file mode 100755 index 0000000..8fa03e5 --- /dev/null +++ b/src/transferpak/TransferPakRomReader.cpp @@ -0,0 +1,98 @@ +#include "transferpak/TransferPakRomReader.h" +#include "transferpak/TransferPakManager.h" + +static uint16_t GB_BANK_SIZE = 0x4000; + +static uint16_t calculateBytesLeftInCurrentBank(uint32_t currentRomOffset) +{ + const uint16_t bankOffset = static_cast(currentRomOffset % GB_BANK_SIZE); + return GB_BANK_SIZE - bankOffset; +} + +// gameboy bank 0 is always mapped to 0x0-0x4000 of the gameboy address space +// gameboy bank 1 is switchable and is mapped to 0x4000-0x8000 of the gameboy address space +// so all rom banks beyond bank 0 need to be accessed in the bank 1 address space +static uint16_t calculateGBAddressForRomOffset(uint32_t romOffset) +{ + if(romOffset < GB_BANK_SIZE) + { + return static_cast(romOffset); + } + else + { + return static_cast(GB_BANK_SIZE + (romOffset % GB_BANK_SIZE)); + } +} + +TransferPakRomReader::TransferPakRomReader(TransferPakManager& pakManager) + : pakManager_(pakManager) + , currentRomOffset_(0) +{ +} + +TransferPakRomReader::~TransferPakRomReader() +{ +} + +bool TransferPakRomReader::readByte(uint8_t& outByte) +{ + return read(&outByte, 1); +} + +bool TransferPakRomReader::read(uint8_t* outBuffer, uint32_t bytesToRead) +{ + uint32_t bytesRemaining = bytesToRead; + uint16_t bytesLeftInCurrentBank; + uint16_t currentRead; + + while(bytesRemaining > 0) + { + bytesLeftInCurrentBank = calculateBytesLeftInCurrentBank(currentRomOffset_); + currentRead = (bytesRemaining > bytesLeftInCurrentBank) ? bytesLeftInCurrentBank : static_cast(bytesRemaining); + + pakManager_.read(calculateGBAddressForRomOffset(currentRomOffset_), outBuffer, currentRead); + outBuffer += currentRead; + bytesRemaining -= currentRead; + + advance(currentRead); + } + return true; +} + +uint8_t TransferPakRomReader::peek() +{ + uint8_t buffer[1]; + pakManager_.read(calculateGBAddressForRomOffset(currentRomOffset_), buffer, 1); + return buffer[0]; +} + +bool TransferPakRomReader::advance(uint32_t numBytes) +{ + return seek(currentRomOffset_ + numBytes); +} + +bool TransferPakRomReader::seek(uint32_t absoluteOffset) +{ + const uint8_t previousBankIndex = getCurrentBankIndex(); + uint8_t newBankIndex; + + currentRomOffset_ = absoluteOffset; + newBankIndex = getCurrentBankIndex(); + + if(previousBankIndex != newBankIndex) + { + pakManager_.switchGBROMBank(newBankIndex); + } + return true; +} + +bool TransferPakRomReader::searchFor(const uint8_t* needle, uint32_t needleLength) +{ + // NOT IMPLEMENTED + return false; +} + +uint8_t TransferPakRomReader::getCurrentBankIndex() const +{ + return static_cast(currentRomOffset_ / GB_BANK_SIZE); +} \ No newline at end of file diff --git a/src/transferpak/TransferPakSaveManager.cpp b/src/transferpak/TransferPakSaveManager.cpp new file mode 100755 index 0000000..1fff84f --- /dev/null +++ b/src/transferpak/TransferPakSaveManager.cpp @@ -0,0 +1,118 @@ +#include "transferpak/TransferPakSaveManager.h" +#include "transferpak/TransferPakManager.h" + +static uint16_t GB_SRAM_BANK_SIZE = 0x2000; + +static uint16_t getSRAMBankOffset(uint32_t absoluteSRAMOffset) +{ + return static_cast(absoluteSRAMOffset % GB_SRAM_BANK_SIZE); +} + +static uint16_t calculateBytesLeftInCurrentBank(uint32_t SRAMOffset) +{ + const uint16_t bankOffset = getSRAMBankOffset(SRAMOffset); + return GB_SRAM_BANK_SIZE - bankOffset; +} + +TransferPakSaveManager::TransferPakSaveManager(TransferPakManager& pakManager) + : pakManager_(pakManager) + , sramOffset_(0) +{ +} + +TransferPakSaveManager::~TransferPakSaveManager() +{ +} + +bool TransferPakSaveManager::readByte(uint8_t& outByte) +{ + return read(&outByte, 1); +} + +void TransferPakSaveManager::writeByte(uint8_t byte) +{ + write(&byte, 1); +} + +void TransferPakSaveManager::write(const uint8_t* buffer, uint32_t bytesToWrite) +{ + uint32_t bytesRemaining = bytesToWrite; + uint16_t bytesLeftInCurrentBank; + uint16_t currentWrite; + uint16_t bankOffset; + + while(bytesRemaining > 0) + { + bytesLeftInCurrentBank = calculateBytesLeftInCurrentBank(sramOffset_); + currentWrite = (bytesRemaining > bytesLeftInCurrentBank) ? bytesLeftInCurrentBank : static_cast(bytesRemaining); + bankOffset = getSRAMBankOffset(sramOffset_); + + pakManager_.writeSRAM(bankOffset, buffer, currentWrite); + buffer += currentWrite; + bytesRemaining -= currentWrite; + + advance(currentWrite); + } +} + +bool TransferPakSaveManager::read(uint8_t* outBuffer, uint32_t bytesToRead) +{ + uint32_t bytesRemaining = bytesToRead; + uint16_t bytesLeftInCurrentBank; + uint16_t currentRead; + uint16_t bankOffset; + + while(bytesRemaining > 0) + { + bytesLeftInCurrentBank = calculateBytesLeftInCurrentBank(sramOffset_); + currentRead = (bytesRemaining > bytesLeftInCurrentBank) ? bytesLeftInCurrentBank : static_cast(bytesRemaining); + bankOffset = getSRAMBankOffset(sramOffset_); + + pakManager_.readSRAM(bankOffset, outBuffer, currentRead); + outBuffer += currentRead; + bytesRemaining -= currentRead; + + advance(currentRead); + } + return true; +} + +uint8_t TransferPakSaveManager::peek() +{ + uint8_t buffer[1]; + const uint16_t bankOffset = getSRAMBankOffset(sramOffset_); + pakManager_.readSRAM(bankOffset, buffer, 1); + return buffer[0]; +} + +bool TransferPakSaveManager::advance(uint32_t numBytes) +{ + return seek(sramOffset_ + numBytes); +} + +bool TransferPakSaveManager::rewind(uint32_t numBytes) +{ + return seek(sramOffset_ - numBytes); +} + +bool TransferPakSaveManager::seek(uint32_t absoluteOffset) +{ + const uint8_t previousBankIndex = getCurrentBankIndex(); + uint8_t newBankIndex; + + sramOffset_ = absoluteOffset; + newBankIndex = getCurrentBankIndex(); + +// debugf("[TransferPakSaveManager]: %s(%lx) -> previousBankIndex %hu, newBankIndex %hu\r\n", __FUNCTION__, absoluteOffset, previousBankIndex, newBankIndex); + + if(previousBankIndex != newBankIndex) + { + pakManager_.switchGBSRAMBank(newBankIndex); + } + return true; +} + +uint8_t TransferPakSaveManager::getCurrentBankIndex() const +{ + return static_cast(sramOffset_ / GB_SRAM_BANK_SIZE); +} \ No newline at end of file diff --git a/src/widget/CursorWidget.cpp b/src/widget/CursorWidget.cpp new file mode 100755 index 0000000..4f74672 --- /dev/null +++ b/src/widget/CursorWidget.cpp @@ -0,0 +1,130 @@ +#include "widget/CursorWidget.h" +#include "animations/AnimationManager.h" +#include "core/RDPQGraphics.h" + +static void moveAnimationFinishedCallback(void* context) +{ + CursorWidget *thiz = (CursorWidget*)context; + thiz->onMoveAnimationFinished(); +} + +CursorWidget::CursorWidget(AnimationManager& animationManager) + : idleAnimation_(this) + , moveAnimation_(this) + , animManager_(animationManager) + , style_({0}) + , bounds_({0}) + , visible_(true) +{ + idleAnimation_.setLoopType(AnimationLoopType::BACK_AND_FORTH); + moveAnimation_.setAnimationFinishedCallback(this, moveAnimationFinishedCallback); + + // idleAnimation is active first + animManager_.add(&idleAnimation_); +} + +CursorWidget::~CursorWidget() +{ + animManager_.remove(&moveAnimation_); + animManager_.remove(&idleAnimation_); +} + +bool CursorWidget::isFocused() const +{ + //irrelevant + return true; +} + +void CursorWidget::setFocused(bool) +{ + //irrelevant +} + +bool CursorWidget::isVisible() const +{ + return visible_; +} + +void CursorWidget::setVisible(bool visible) +{ + visible_ = visible; +} + +Rectangle CursorWidget::getBounds() const +{ + return bounds_; +} + +void CursorWidget::setBounds(const Rectangle& bounds) +{ + bounds_ = bounds; +} + +void CursorWidget::moveToBounds(const Rectangle& targetBounds) +{ + animManager_.remove(&idleAnimation_); + + if(moveAnimation_.isFinished()) + { + // moveAnimation is in the finished state. + // therefore it wasn't part of AnimationManager yet + animManager_.add(&moveAnimation_); + } + else + { + // previous move animation hasn't finished. + // this means it's also already registered on AnimationManager + // just skip it to its end position before starting a new animation + moveAnimation_.skipToEnd(); + } + + const Rectangle diffVector = { + .x = targetBounds.x - bounds_.x, + .y = targetBounds.y - bounds_.y, + .width = targetBounds.width - bounds_.width, + .height = targetBounds.height - bounds_.height + }; + + moveAnimation_.start(bounds_, diffVector, style_.moveAnimationDurationInMs); +} + +Dimensions CursorWidget::getSize() const +{ + return Dimensions{ + .width = bounds_.width, + .height = bounds_.height + }; +} + +bool CursorWidget::handleUserInput(const joypad_inputs_t&) +{ + // irrelevant + return false; +} + +void CursorWidget::render(RDPQGraphics& gfx, const Rectangle& parentBounds) +{ + if(!visible_ || !style_.sprite) + { + return; + } + const Rectangle myBounds = addOffset(bounds_, parentBounds); + + gfx.drawSprite(myBounds, style_.sprite, style_.spriteSettings); +} + +void CursorWidget::setStyle(const CursorStyle& style) +{ + style_ = style; +} + +void CursorWidget::onMoveAnimationFinished() +{ + // when the move animation is done, remove it from AnimationManager + // and replace it with the idleAnimation + animManager_.remove(&moveAnimation_); + animManager_.add(&idleAnimation_); + + // reset idle animation starting from the new bounds_ + idleAnimation_.start(bounds_, style_.idleMoveDiff, style_.idleAnimationDurationInMs); +} diff --git a/src/widget/DialogWidget.cpp b/src/widget/DialogWidget.cpp new file mode 100755 index 0000000..ab4361e --- /dev/null +++ b/src/widget/DialogWidget.cpp @@ -0,0 +1,192 @@ +#include "widget/DialogWidget.h" +#include "core/RDPQGraphics.h" + +#include + +DialogWidget::DialogWidget(AnimationManager& animationManager) + : animationManager_(animationManager) + , bounds_({0}) + , style_({0}) + , data_(nullptr) + , onDialogFinishedCb_(nullptr) + , onDialogFinishedCbContext_(nullptr) + , focused_(false) + , visible_(true) + , btnAPressedOnPrevCheck_(false) +{ +} + +DialogWidget::~DialogWidget() +{ +} + +const DialogWidgetStyle& DialogWidget::getStyle() const +{ + return style_; +} + +void DialogWidget::setStyle(const DialogWidgetStyle& style) +{ + style_ = style; +} + +void DialogWidget::setData(DialogData* data) +{ + data_ = data; +} + +void DialogWidget::appendDialogData(DialogData* data) +{ + if(!data_) + { + setData(data); + return; + } + + DialogData* entry = data_; + while(entry->next) + { + entry = entry->next; + } + entry->next = data; +} + +bool DialogWidget::isFocused() const +{ + return focused_; +} + +void DialogWidget::setFocused(bool focused) +{ + focused_ = focused; +} + +bool DialogWidget::isVisible() const +{ + return visible_; +} + +void DialogWidget::setVisible(bool visible) +{ + visible_ = visible; +} + +Rectangle DialogWidget::getBounds() const +{ + return bounds_; +} + +void DialogWidget::setBounds(const Rectangle& bounds) +{ + bounds_ = bounds; +} + +Dimensions DialogWidget::getSize() const +{ + return Dimensions{.width = bounds_.width, .height = bounds_.height}; +} + +void DialogWidget::setOnDialogFinishedCallback(void (*onDialogFinishedCb)(void*), void* context) +{ + onDialogFinishedCb_ = onDialogFinishedCb; + onDialogFinishedCbContext_ = context; +} + +void DialogWidget::advanceDialog() +{ + if(!data_ || !data_->next) + { + if(onDialogFinishedCb_) + { + onDialogFinishedCb_(onDialogFinishedCbContext_); + } + return; + } + const DialogData* oldEntry = data_; + data_ = data_->next; + + if(oldEntry->shouldReleaseWhenDone) + { + delete oldEntry; + oldEntry = nullptr; + } +} + +bool DialogWidget::handleUserInput(const joypad_inputs_t& userInput) +{ + // make sure the user needs to release the button before handling the A button again + // if we don't do that, a different component might react to the same a button press + if(btnAPressedOnPrevCheck_) + { + if(!userInput.btn.a) + { + advanceDialog(); + btnAPressedOnPrevCheck_ = false; + } + } + else if(isAdvanceAllowed() && userInput.btn.a) + { + btnAPressedOnPrevCheck_ = true; + } + + return false; +} + +void DialogWidget::render(RDPQGraphics& gfx, const Rectangle& parentBounds) +{ + if(!visible_) + { + return; + } + + const Rectangle myBounds = addOffset(bounds_, parentBounds); + // render the background first, if any. + if(style_.backgroundSprite) + { + gfx.drawSprite(myBounds, style_.backgroundSprite, style_.backgroundSpriteSettings); + } + + if(!data_) + { + return; + } + + if(data_->characterSprite && data_->characterSpriteVisible) + { + const Rectangle absoluteCharBounds = addOffset(data_->characterSpriteBounds, myBounds); + gfx.drawSprite(absoluteCharBounds, data_->characterSprite, data_->characterSpriteSettings); + } + + if(data_->buttonSprite && data_->buttonSpriteVisible) + { + const Rectangle absoluteButtonSpriteBounds = addOffset(data_->buttonSpriteBounds, myBounds); + gfx.drawSprite(absoluteButtonSpriteBounds, data_->buttonSprite, data_->buttonSpriteSettings); + } + + if(data_->text[0] != '\0') + { + const Rectangle textBounds = { + .x = myBounds.x + style_.marginLeft, + .y = myBounds.y + style_.marginTop, + .width = myBounds.width - style_.marginLeft - style_.marginRight, + .height = myBounds.height - style_.marginTop - style_.marginBottom + }; + + gfx.drawText(textBounds, data_->text, style_.textSettings); + } +} + +bool DialogWidget::isAdvanceAllowed() const +{ + return (!data_ || !data_->userAdvanceBlocked); +} + +void setDialogDataText(DialogData& data, const char* format, ...) +{ + va_list argList; + va_start(argList, format); + + vsnprintf(data.text, DIALOG_TEXT_SIZE, format, argList); + + va_end(argList); +} diff --git a/src/widget/IFocusListener.cpp b/src/widget/IFocusListener.cpp new file mode 100755 index 0000000..e85fe07 --- /dev/null +++ b/src/widget/IFocusListener.cpp @@ -0,0 +1,5 @@ +#include "widget/IFocusListener.h" + +IFocusListener::~IFocusListener() +{ +} diff --git a/src/widget/MenuItemWidget.cpp b/src/widget/MenuItemWidget.cpp new file mode 100755 index 0000000..c84a29e --- /dev/null +++ b/src/widget/MenuItemWidget.cpp @@ -0,0 +1,110 @@ +#include "widget/MenuItemWidget.h" +#include "core/RDPQGraphics.h" + +MenuItemWidget::MenuItemWidget() + : data_() + , style_() + , focused_(false) + , visible_(true) + , aButtonPressed_(false) +{ +} + +MenuItemWidget::~MenuItemWidget() +{ +} + +void MenuItemWidget::setData(const MenuItemData& data) +{ + data_ = data; +} + +void MenuItemWidget::setStyle(const MenuItemStyle& style) +{ + style_ = style; +} + +bool MenuItemWidget::isFocused() const +{ + return focused_; +} + +void MenuItemWidget::setFocused(bool focused) +{ + focused_ = focused; +} + +bool MenuItemWidget::isVisible() const +{ + return visible_; +} + +void MenuItemWidget::setVisible(bool visible) +{ + visible_ = visible; +} + +Rectangle MenuItemWidget::getBounds() const +{ + return Rectangle{.x = 0, .y = 0, .width = style_.size.width, .height = style_.size.height}; +} + +void MenuItemWidget::setBounds(const Rectangle& bounds) +{ + // Not relevant: the actual bounds are passed from the VerticalList widget +} + +Dimensions MenuItemWidget::getSize() const +{ + return style_.size; +} + +bool MenuItemWidget::handleUserInput(const joypad_inputs_t& userInput) +{ + // only handle button release, otherwise you'll be in trouble on scene transitions: + // the user can't release the button fast enough, so the same press would get handled twice. + if(userInput.btn.a) + { + aButtonPressed_ = true; + return true; + } + else if(aButtonPressed_) + { + execute(); + aButtonPressed_ = false; + return true; + } + return false; +} + +void MenuItemWidget::render(RDPQGraphics& gfx, const Rectangle& parentBounds) +{ + if(!visible_) + { + return; + } + Rectangle myBounds = {.x = parentBounds.x, .y = parentBounds.y, .width = style_.size.width, .height = style_.size.height}; + if(style_.backgroundSprite) + { + gfx.drawSprite(myBounds, style_.backgroundSprite, style_.backgroundSpriteSettings); + } + + if(style_.iconSprite) + { + const Rectangle iconSpriteBounds = addOffset(style_.iconSpriteBounds, myBounds); + gfx.drawSprite(iconSpriteBounds, style_.iconSprite, style_.iconSpriteSettings); + } + + myBounds.x += style_.leftMargin; + myBounds.y += style_.topMargin; + // account for leftMargin and topMargin twice (we also apply it for the rightmargin and bottom) + myBounds.width -= style_.leftMargin - style_.leftMargin; + myBounds.height -= style_.topMargin - style_.topMargin; + + gfx.drawText(myBounds, data_.title, (focused_) ? style_.titleFocused : style_.titleNotFocused); +} + +void MenuItemWidget::execute() +{ + data_.onConfirmAction(data_.context, data_.itemParam); +} \ No newline at end of file diff --git a/src/widget/TransferPakDetectionWidget.cpp b/src/widget/TransferPakDetectionWidget.cpp new file mode 100755 index 0000000..da694b0 --- /dev/null +++ b/src/widget/TransferPakDetectionWidget.cpp @@ -0,0 +1,241 @@ +#include "widget/TransferPakDetectionWidget.h" +#include "transferpak/TransferPakManager.h" +#include "transferpak/TransferPakRomReader.h" +#include "transferpak/TransferPakSaveManager.h" + +#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) + , tpakManager_(pakManager) + , bounds_({0}) + , textBounds_({.x = 0, .y = 0, .width = 200, .height = 20}) + , currentState_(TransferPakWidgetState::UNKNOWN) + , previousInputState_({0}) + , gen1Type_(Gen1GameType::INVALID) + , gen2Type_(Gen2GameType::INVALID) + , stateChangedCallback_(nullptr) + , stateChangedCallbackContext_(nullptr) + , focused_(false) + , visible_(true) +{ +} + +TransferPakDetectionWidget::~TransferPakDetectionWidget() +{ +} + +bool TransferPakDetectionWidget::isFocused() const +{ + return focused_; +} + +void TransferPakDetectionWidget::setFocused(bool focused) +{ + focused_ = focused; +} + +bool TransferPakDetectionWidget::isVisible() const +{ + return visible_; +} + +void TransferPakDetectionWidget::setVisible(bool visible) +{ + visible_ = visible; +} + +Rectangle TransferPakDetectionWidget::getBounds() const +{ + return bounds_; +} + +void TransferPakDetectionWidget::setBounds(const Rectangle& bounds) +{ + bounds_ = bounds; +} + +Dimensions TransferPakDetectionWidget::getSize() const +{ + return Dimensions{ .width = bounds_.width, .height = bounds_.height }; +} + +bool TransferPakDetectionWidget::handleUserInput(const joypad_inputs_t& userInput) +{ + bool ret = false; + if(previousInputState_.btn.a && !userInput.btn.a) + { + switch(currentState_) + { + case TransferPakWidgetState::UNKNOWN: + switchState(currentState_, TransferPakWidgetState::DETECTING_PAK); + ret = true; + break; + default: + break; + } + } + + previousInputState_ = userInput; + return ret; +} + +void TransferPakDetectionWidget::render(RDPQGraphics& gfx, const Rectangle& parentBounds) +{ + switch(currentState_) + { + case TransferPakWidgetState::UNKNOWN: + renderUnknownState(gfx, parentBounds); + break; + case TransferPakWidgetState::NO_TRANSFER_PAK_FOUND: + case TransferPakWidgetState::GB_HEADER_VALIDATION_FAILED: + case TransferPakWidgetState::NO_GAME_FOUND: + renderErrorState(gfx, parentBounds); + default: + break; + } +} + +TransferPakWidgetState TransferPakDetectionWidget::getState() const +{ + return currentState_; +} + +void TransferPakDetectionWidget::retrieveGameType(Gen1GameType& outGen1Type, Gen2GameType& outGen2Type) +{ + outGen1Type = gen1Type_; + outGen2Type = gen2Type_; +} + +void TransferPakDetectionWidget::setStyle(const TransferPakDetectionWidgetStyle& style) +{ + style_ = style; +} + +void TransferPakDetectionWidget::setStateChangedCallback(void (*callback)(void*, TransferPakWidgetState), void* context) +{ + stateChangedCallback_ = callback; + stateChangedCallbackContext_ = context; +} + +void TransferPakDetectionWidget::switchState(TransferPakWidgetState previousState, TransferPakWidgetState state) +{ + TransferPakWidgetState newState; + bool ret; + + currentState_ = state; + switch(state) + { + case TransferPakWidgetState::DETECTING_PAK: + ret = selectTransferPak(); + newState = (ret) ? TransferPakWidgetState::VALIDATING_GB_HEADER : TransferPakWidgetState::NO_TRANSFER_PAK_FOUND; + switchState(state, newState); + return; + case TransferPakWidgetState::VALIDATING_GB_HEADER: + ret = validateGameboyHeader(); + newState = (ret) ? TransferPakWidgetState::DETECTING_GAME : TransferPakWidgetState::GB_HEADER_VALIDATION_FAILED; + switchState(state, newState); + return; + case TransferPakWidgetState::DETECTING_GAME: + ret = detectGameType(); + newState = (ret) ? TransferPakWidgetState::GAME_FOUND : TransferPakWidgetState::NO_GAME_FOUND; + switchState(state, newState); + return; + case TransferPakWidgetState::GAME_FOUND: +// doRandomShit(tpakManager_); + break; + default: + break; + } + + // now notify the callback (if any) that the state has changed + if(stateChangedCallback_) + { + stateChangedCallback_(stateChangedCallbackContext_, state); + } +} + +void TransferPakDetectionWidget::renderUnknownState(RDPQGraphics& gfx, const Rectangle& parentBounds) +{ + const Rectangle absoluteTextBounds = addOffset(textBounds_, bounds_); + gfx.drawText(absoluteTextBounds, "Press A to check the transfer pak...", style_.textSettings); +} + +void TransferPakDetectionWidget::renderErrorState(RDPQGraphics& gfx, const Rectangle& parentBounds) +{ + const Rectangle absoluteTextBounds = addOffset(textBounds_, bounds_); + const char* errorText; + + switch(currentState_) + { + case TransferPakWidgetState::NO_TRANSFER_PAK_FOUND: + errorText = "ERROR: No Transfer Pak found!"; + break; + case TransferPakWidgetState::GB_HEADER_VALIDATION_FAILED: + errorText = "ERROR: Gameboy Header validation failed!"; + break; + case TransferPakWidgetState::NO_GAME_FOUND: + // TODO: technically this is not correct + // We just didn't find a Pkmn game. + errorText = "ERROR: No game found!"; + break; + default: + errorText = "ERROR: this should never happen!"; + break; + } + + gfx.drawText(absoluteTextBounds, errorText, style_.textSettings); +} + +bool TransferPakDetectionWidget::selectTransferPak() +{ + joypad_poll(); + for(uint8_t i=0; i < 4; ++i) + { + tpakManager_.setPort((joypad_port_t)i); + if(tpakManager_.hasTransferPak()) + { + debugf("[Application]: Transfer pak found at controller %hu\r\n", i); + // power the transfer pak off in case it was powered before + tpakManager_.setPower(false); + return true; + } + } + debugf("[Application]: ERROR: no transfer pak found!\r\n"); + return false; +} + +bool TransferPakDetectionWidget::validateGameboyHeader() +{ + if(!tpakManager_.setPower(true)) + { + return false; + } + return tpakManager_.validateGbHeader(); +} + +bool TransferPakDetectionWidget::detectGameType() +{ + GameboyCartridgeHeader cartridgeHeader; + TransferPakRomReader romReader(tpakManager_); + + readGameboyCartridgeHeader(romReader, cartridgeHeader); + + gen1Type_ = gen1_determineGameType(cartridgeHeader); + gen2Type_ = gen2_determineGameType(cartridgeHeader); + + return (gen1Type_ != Gen1GameType::INVALID || gen2Type_ != Gen2GameType::INVALID); +} \ No newline at end of file diff --git a/src/widget/VerticalList.cpp b/src/widget/VerticalList.cpp new file mode 100755 index 0000000..7b0049b --- /dev/null +++ b/src/widget/VerticalList.cpp @@ -0,0 +1,439 @@ +#include "widget/VerticalList.h" +#include "widget/IWidget.h" +#include "widget/IFocusListener.h" +#include "core/RDPQGraphics.h" +#include "animations/AnimationManager.h" +#include "core/DragonUtils.h" + +#include +#include +#include + +static bool isWidgetInsideWindow(const Rectangle& widgetBounds, uint32_t listHeight, uint32_t windowStartY) +{ + const int correctedY = widgetBounds.y - windowStartY; + // we want to have the entire widget in the view, so in a vertical list, that means the bottom of the widget + // needs to be inside the window +// debugf("%s: bounds [%d, %d, %d, %d], listHeight %lu, windowStartY %lu, correctedY %d\n", __FUNCTION__, widgetBounds.x, widgetBounds.y, widgetBounds.width, widgetBounds.height, listHeight, windowStartY, correctedY); + return (correctedY >= 0) && (correctedY + widgetBounds.height <= static_cast(listHeight)); +} + +static int32_t getVerticalWindowScrollNeededToMakeWidgetFullyVisible(const Rectangle& widgetBounds, uint32_t listHeight, uint32_t windowStartY) +{ + if(widgetBounds.y < static_cast(windowStartY)) + { + return widgetBounds.y - static_cast(windowStartY); + } + + const int32_t widgetEndY = widgetBounds.y + widgetBounds.height; + const int32_t listEndY = static_cast(windowStartY + listHeight); + if(widgetEndY > listEndY) + { + return widgetEndY - listEndY; + } + return 0; +} + +/** + * @brief The widgetBoundsList_ contains the coordinates of the widgets starting from the top of the list + * it assumes there's no such thing as a view window and those coordinates do not take the VerticalList bounds into account. + * They just start at x=0, y=0 and keep increasing y until the last widget. + * + * But this function maps these coordinates to the actual absolute coordinates on screen + * by taking the view window y coordinate into account (which also doesn't take the VerticalList bounds into account) + * and adding the + * + * @param widgetBounds The bounds of the widget from the widgetBoundList_ + * @param windowStartY the start y position of the view window in the widgetBoundList_ coordinate system + * @param widgetTopX The absolute left top corner x position from the widget where the first item is supposed to start + * @param widgetTopY The absolute left top corner y position from the widget where the first item is supposed to start + * @return const Rectangle + */ +static const Rectangle calculateListWidgetBounds(const Rectangle& widgetBounds, uint32_t windowStartY, int widgetTopX, int widgetTopY) +{ + return { + .x = widgetTopX + widgetBounds.x, + .y = widgetTopY + widgetBounds.y - static_cast(windowStartY), + .width = widgetBounds.width, + .height = widgetBounds.height + }; +} + +static uint32_t getInnerListHeight(const Rectangle& listBounds, int marginTop, int marginBottom) +{ + return listBounds.height - marginTop - marginBottom; +} + +MoveVerticalListWindowAnimation::MoveVerticalListWindowAnimation(VerticalList* list) + : AbstractAnimation(1.f) // start in the finished state by specifying the 1.f end pos initially + , list_(list) + , windowStartY_(0) + , windowEndY_(0) +{ +} + +MoveVerticalListWindowAnimation::~MoveVerticalListWindowAnimation() +{ +} + +AnimationDistanceTimeFunctionType MoveVerticalListWindowAnimation::getDistanceTimeFunctionType() const +{ + return AnimationDistanceTimeFunctionType::EASE_IN_EASE_OUT; +} + +uint32_t MoveVerticalListWindowAnimation::getDurationInMs() const +{ + return 250; +} + +void MoveVerticalListWindowAnimation::start(uint32_t windowStartY, uint32_t windowEndY) +{ + currentTimePos_ = 0.f; + windowStartY_ = static_cast(windowStartY); + windowEndY_ = static_cast(windowEndY); +} + +void MoveVerticalListWindowAnimation::apply(float pos) +{ + // we could need a negative value here if windowEndY < windowStartY. But our member windowEndY is unsigned. + const uint32_t newWindowStart = static_cast(ceilf(windowStartY_ + (pos * (windowEndY_ - windowStartY_)))); + list_->setViewWindowStartY(newWindowStart); +} + +VerticalList::VerticalList(AnimationManager& animationManager) + : moveWindowAnimation_(this) + , widgetList_() + , widgetBoundsList_() + , focusListeners_() + , listStyle_({0}) + , bounds_({0}) + , windowMinY_(0) + , focusedWidgetIndex_(0) + , animManager_(animationManager) + , focused_(false) + , visible_(true) +{ + animManager_.add(&moveWindowAnimation_); +} + +VerticalList::~VerticalList() +{ + animManager_.remove(&moveWindowAnimation_); +} + +bool VerticalList::focusNext() +{ + FocusChangeStatus changeStatus; + if(focusedWidgetIndex_ + 1 >= widgetList_.size()) + { + return false; + } + // finish previous animation first (skip it) to ensure windowMinY_ is set correctly + moveWindowAnimation_.skipToEnd(); + + changeStatus.prevFocus = widgetList_[focusedWidgetIndex_]; + widgetList_[focusedWidgetIndex_]->setFocused(false); + + ++focusedWidgetIndex_; + + changeStatus.curFocus = widgetList_[focusedWidgetIndex_]; + changeStatus.focusBounds = calculateListWidgetBounds(widgetBoundsList_[focusedWidgetIndex_], windowMinY_, bounds_.x + listStyle_.marginLeft, bounds_.y + listStyle_.marginTop); + widgetList_[focusedWidgetIndex_]->setFocused(true); + + const int32_t scrollAmountY = scrollWindowToFocusedWidget(); + changeStatus.focusBounds.y -= scrollAmountY; + + notifyFocusListeners(changeStatus); + + return true; +} + +bool VerticalList::focusPrevious() +{ + FocusChangeStatus changeStatus; + if(focusedWidgetIndex_ == 0) + { + return false; + } + + // finish previous animation first (skip it) to ensure windowMinY_ is set correctly + moveWindowAnimation_.skipToEnd(); + + changeStatus.prevFocus = widgetList_[focusedWidgetIndex_]; + widgetList_[focusedWidgetIndex_]->setFocused(false); + + --focusedWidgetIndex_; + + changeStatus.curFocus = widgetList_[focusedWidgetIndex_]; + changeStatus.focusBounds = calculateListWidgetBounds(widgetBoundsList_[focusedWidgetIndex_], windowMinY_, bounds_.x + listStyle_.marginLeft, bounds_.y + listStyle_.marginTop); + widgetList_[focusedWidgetIndex_]->setFocused(true); + + const int32_t scrollAmountY = scrollWindowToFocusedWidget(); + changeStatus.focusBounds.y -= scrollAmountY; + + notifyFocusListeners(changeStatus); + + return true; +} + +void VerticalList::addWidget(IWidget *widget) +{ + const Dimensions widgetSize = widget->getSize(); + widgetList_.push_back(widget); + + if (widgetList_.size() == 1) + { + widgetBoundsList_.push_back(Rectangle{.x = 0, .y = 0, .width = widgetSize.width, .height = widgetSize.height}); + widget->setFocused(focused_); + + FocusChangeStatus changeStatus = { + .focusBounds = calculateListWidgetBounds(widgetBoundsList_[focusedWidgetIndex_], windowMinY_, bounds_.x + listStyle_.marginLeft, bounds_.y + listStyle_.marginTop), + .prevFocus = nullptr, + .curFocus = widget + }; + notifyFocusListeners(changeStatus); + } + else + { + const Rectangle lastWidgetBounds = widgetBoundsList_.back(); + widgetBoundsList_.push_back(Rectangle{.x = 0, .y = lastWidgetBounds.y + lastWidgetBounds.height + listStyle_.verticalSpacingBetweenWidgets, .width = widgetSize.width, .height = widgetSize.height}); + } +} + +void VerticalList::clearWidgets() +{ + widgetList_.clear(); + widgetBoundsList_.clear(); +} + +void VerticalList::setStyle(const VerticalListStyle& style) +{ + listStyle_ = style; + rebuildLayout(); +} + +void VerticalList::setViewWindowStartY(uint32_t windowStartY) +{ + windowMinY_ = windowStartY; +} + +bool VerticalList::isFocused() const +{ + return focused_; +} + +void VerticalList::setFocused(bool isFocused) +{ + focused_ = isFocused; + if(widgetList_.empty()) + { + return; + } + widgetList_[focusedWidgetIndex_]->setFocused(focused_); + + FocusChangeStatus changeStatus = { + .focusBounds = calculateListWidgetBounds(widgetBoundsList_[focusedWidgetIndex_], windowMinY_, bounds_.x + listStyle_.marginLeft, bounds_.y + listStyle_.marginTop) + }; + + if(isFocused) + { + changeStatus.prevFocus = nullptr; + changeStatus.curFocus = widgetList_[focusedWidgetIndex_]; + } + else + { + changeStatus.prevFocus = widgetList_[focusedWidgetIndex_]; + changeStatus.curFocus = nullptr; + } + + notifyFocusListeners(changeStatus); +} + +bool VerticalList::isVisible() const +{ + return visible_; +} + +void VerticalList::setVisible(bool visible) +{ + visible_ = visible; +} + +Rectangle VerticalList::getBounds() const +{ + return bounds_; +} + +void VerticalList::setBounds(const Rectangle &bounds) +{ + bounds_ = bounds; + rebuildLayout(); +} + +Dimensions VerticalList::getSize() const +{ + return getDimensions(bounds_); +} + +bool VerticalList::handleUserInput(const joypad_inputs_t& userInput) +{ + if(widgetList_.empty()) + { + return false; + } + + const bool hasFocusedWidgetHandledInput = widgetList_[focusedWidgetIndex_]->handleUserInput(userInput); + + if(hasFocusedWidgetHandledInput) + { + return hasFocusedWidgetHandledInput; + } + + const UINavigationKey navKey = determineUINavigationKey(userInput, NavigationInputSourceType::BOTH); + + if(navKey == UINavigationKey::UP) + { + if(focusedWidgetIndex_ < 1) + { + return false; + } + return focusPrevious(); + } + else if(navKey == UINavigationKey::DOWN) + { + if(focusedWidgetIndex_ == widgetList_.size() - 1) + { + return false; + } + return focusNext(); + } + + return false; +} + +void VerticalList::render(RDPQGraphics& gfx, const Rectangle& parentBounds) +{ + if(!visible_) + { + return; + } + + const uint32_t innerListHeight = getInnerListHeight(bounds_, listStyle_.marginTop, listStyle_.marginBottom); + uint32_t i; + const Rectangle myBounds = addOffset(bounds_, parentBounds); + const int topX = myBounds.x + listStyle_.marginLeft; + const int topY = myBounds.y + listStyle_.marginTop; + + // store previous clipping rectangle to restore later +// const Rectangle prevClipRect = gfx.getClippingRectangle(); + +// gfx.setClippingRectangle(myBounds); + + // render the background first, if any. + if(listStyle_.backgroundSprite) + { + gfx.drawSprite(myBounds, listStyle_.backgroundSprite, listStyle_.backgroundSpriteSettings); + } + + if(widgetList_.empty()) + { + // restore previous clipping rectangle +// gfx.setClippingRectangle(prevClipRect); + return; + } + + // find the first visible item + for(i = 0; i < widgetList_.size(); ++i) + { + if(isWidgetInsideWindow(widgetBoundsList_[i], innerListHeight, windowMinY_)) + { + // found it + break; + } + } + + if(i > widgetList_.size() - 1) + { + // no items to be rendered + // restore previous clipping rectangle + // gfx.setClippingRectangle(prevClipRect); + return; + } + + // now start rendering the widgets +// debugf("[VerticalList]: %s: now start rendering the widgets at i=%lu, widgetList_ size %lu, widgetBoundsList_ size %lu\r\n", __FUNCTION__, i, static_cast(widgetList_.size()), static_cast(widgetBoundsList_.size())); + do + { + const Rectangle listBounds = calculateListWidgetBounds(widgetBoundsList_[i], windowMinY_, topX, topY); + +// debugf("[VerticalList]: widget %lu: %p\r\n", i, widgetList_[i]); + widgetList_[i]->render(gfx, listBounds); + + ++i; +// debugf("[VerticalList]: next iteration %lu\r\n", i); + } while ((i < widgetList_.size()) && isWidgetInsideWindow(widgetBoundsList_[i], innerListHeight, windowMinY_)); + +// debugf("end loop\r\n"); + // restore previous clipping rectangle +// gfx.setClippingRectangle(prevClipRect); +} + +void VerticalList::registerFocusListener(IFocusListener* focusListener) +{ + focusListeners_.push_back(focusListener); +} + +void VerticalList::unregisterFocusListener(IFocusListener* focusListener) +{ + auto it = std::find(focusListeners_.begin(), focusListeners_.end(), focusListener); + if(it != focusListeners_.end()) + { + focusListeners_.erase(it); + } +} + +void VerticalList::rebuildLayout() +{ + int lastWidgetEndY = 0; + if(widgetList_.empty()) + { + return; + } + + // reset of the widgets + for (size_t i = 0; i < widgetList_.size(); ++i) + { + const Dimensions widgetSize = widgetList_[i]->getSize(); + + widgetBoundsList_[i] = {.x = 0, .y = lastWidgetEndY, .width = widgetSize.width, .height = widgetSize.height}; + lastWidgetEndY += widgetSize.height + listStyle_.verticalSpacingBetweenWidgets; + } +} + +int32_t VerticalList::scrollWindowToFocusedWidget() +{ + //TODO: the new widget is only visible after the scroll is finished. + // the reason is the use of isWidgetInsideWindow() inside the render() function to cull entries from the render window. + // We could potentially eliminate this by expanding the check to allow a partially visible entry to be rendered. + // But for my goals, this is currently not needed, so I claim YAGNI for now. + const uint32_t innerListHeight = getInnerListHeight(bounds_, listStyle_.marginTop, listStyle_.marginBottom); + const int32_t windowScrollYNeeded = getVerticalWindowScrollNeededToMakeWidgetFullyVisible(widgetBoundsList_[focusedWidgetIndex_], innerListHeight, windowMinY_); + if(windowScrollYNeeded != 0) + { + moveWindow(windowScrollYNeeded); + } + return windowScrollYNeeded; +} + +void VerticalList::moveWindow(int32_t yAmount) +{ + moveWindowAnimation_.start(windowMinY_, windowMinY_ + yAmount); +} + +void VerticalList::notifyFocusListeners(const FocusChangeStatus& status) +{ + for(IFocusListener* listener : focusListeners_) + { + listener->focusChanged(status); + } +} \ No newline at end of file