mirror of
https://github.com/risingPhil/PokeMe64.git
synced 2026-03-21 18:04:15 -05:00
Initial import of the first (extremely early) functional version.
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)
This commit is contained in:
parent
03e87d734b
commit
e51726eef9
6
.gitmodules
vendored
Normal file
6
.gitmodules
vendored
Normal file
|
|
@ -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
|
||||
16
CREDITS.md
Executable file
16
CREDITS.md
Executable file
|
|
@ -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.
|
||||
|
||||
218
LICENSE
Normal file → Executable file
218
LICENSE
Normal file → Executable file
|
|
@ -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.
|
||||
|
|
|
|||
57
Makefile
Executable file
57
Makefile
Executable file
|
|
@ -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)
|
||||
73
README.md
Executable file
73
README.md
Executable file
|
|
@ -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. :-)
|
||||
|
||||
|
||||
BIN
assets/Arial.ttf
Executable file
BIN
assets/Arial.ttf
Executable file
Binary file not shown.
BIN
assets/hand-cursor.png
Executable file
BIN
assets/hand-cursor.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 4.3 KiB |
BIN
assets/menu-bg-9slice.png
Executable file
BIN
assets/menu-bg-9slice.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 359 B |
BIN
assets/oak.png
Executable file
BIN
assets/oak.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 5.4 KiB |
BIN
assets/pokeball.png
Executable file
BIN
assets/pokeball.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 600 B |
26
include/animations/AnimationManager.h
Executable file
26
include/animations/AnimationManager.h
Executable file
|
|
@ -0,0 +1,26 @@
|
|||
#ifndef _ANIMATIONMANAGER_H
|
||||
#define _ANIMATIONMANAGER_H
|
||||
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
|
||||
class IAnimation;
|
||||
typedef std::vector<IAnimation*> 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
|
||||
144
include/animations/IAnimation.h
Executable file
144
include/animations/IAnimation.h
Executable file
|
|
@ -0,0 +1,144 @@
|
|||
#ifndef _IANIMATION_H
|
||||
#define _IANIMATION_H
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
/**
|
||||
* 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
|
||||
37
include/animations/MoveAnimation.h
Executable file
37
include/animations/MoveAnimation.h
Executable file
|
|
@ -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
|
||||
29
include/core/Application.h
Executable file
29
include/core/Application.h
Executable file
|
|
@ -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
|
||||
29
include/core/DragonUtils.h
Executable file
29
include/core/DragonUtils.h
Executable file
|
|
@ -0,0 +1,29 @@
|
|||
#ifndef _DRAGONUTILS_H
|
||||
#define _DRAGONUTILS_H
|
||||
|
||||
#include <libdragon.h>
|
||||
|
||||
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
|
||||
49
include/core/FontManager.h
Executable file
49
include/core/FontManager.h
Executable file
|
|
@ -0,0 +1,49 @@
|
|||
#ifndef _FONTMANAGER_H
|
||||
#define _FONTMANAGER_H
|
||||
|
||||
#include <libdragon.h>
|
||||
#include <unordered_map>
|
||||
#include <string>
|
||||
|
||||
typedef struct FontEntry
|
||||
{
|
||||
rdpq_font_t* font;
|
||||
uint8_t fontId;
|
||||
} FontEntry;
|
||||
|
||||
typedef std::unordered_map<std::string, FontEntry> 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
|
||||
77
include/core/RDPQGraphics.h
Executable file
77
include/core/RDPQGraphics.h
Executable file
|
|
@ -0,0 +1,77 @@
|
|||
#ifndef _RDPQGRAPHICS_H
|
||||
#define _RDPQGRAPHICS_H
|
||||
|
||||
#include "core/common.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <libdragon.h>
|
||||
|
||||
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
|
||||
63
include/core/Sprite.h
Executable file
63
include/core/Sprite.h
Executable file
|
|
@ -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
|
||||
35
include/core/common.h
Executable file
35
include/core/common.h
Executable file
|
|
@ -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
|
||||
15
include/menu/MenuEntries.h
Executable file
15
include/menu/MenuEntries.h
Executable file
|
|
@ -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
|
||||
15
include/menu/MenuFunctions.h
Executable file
15
include/menu/MenuFunctions.h
Executable file
|
|
@ -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
|
||||
83
include/scenes/AbstractUIScene.h
Executable file
83
include/scenes/AbstractUIScene.h
Executable file
|
|
@ -0,0 +1,83 @@
|
|||
#ifndef _ABSTRACTUISCENE_H
|
||||
#define _ABSTRACTUISCENE_H
|
||||
|
||||
#include "scenes/IScene.h"
|
||||
#include <cstdint>
|
||||
|
||||
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
|
||||
83
include/scenes/DistributionPokemonListScene.h
Executable file
83
include/scenes/DistributionPokemonListScene.h
Executable file
|
|
@ -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
|
||||
55
include/scenes/IScene.h
Executable file
55
include/scenes/IScene.h
Executable file
|
|
@ -0,0 +1,55 @@
|
|||
#ifndef _ISCENE_H
|
||||
#define _ISCENE_H
|
||||
|
||||
#include <libdragon.h>
|
||||
|
||||
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
|
||||
44
include/scenes/InitTransferPakScene.h
Executable file
44
include/scenes/InitTransferPakScene.h
Executable file
|
|
@ -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
|
||||
68
include/scenes/MenuScene.h
Executable file
68
include/scenes/MenuScene.h
Executable file
|
|
@ -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<VerticalList, MenuItemData, MenuItemWidget, MenuItemStyle> menuListFiller_;
|
||||
WidgetFocusChainSegment listFocusChainSegment_;
|
||||
uint8_t fontStyleYellowId_;
|
||||
bool bButtonPressed_;
|
||||
private:
|
||||
DialogData singleMessageDialog_;
|
||||
};
|
||||
|
||||
void deleteMenuSceneContext(void* context);
|
||||
|
||||
#endif
|
||||
72
include/scenes/SceneManager.h
Executable file
72
include/scenes/SceneManager.h
Executable file
|
|
@ -0,0 +1,72 @@
|
|||
#ifndef _SCENEMANAGER_H
|
||||
#define _SCENEMANAGER_H
|
||||
|
||||
#include "scenes/IScene.h"
|
||||
#include <vector>
|
||||
|
||||
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<SceneHistorySegment> sceneHistory_;
|
||||
SceneDependencies sceneDeps_;
|
||||
IScene* scene_;
|
||||
SceneType newSceneType_;
|
||||
void* newSceneContext_;
|
||||
void* contextToDelete_;
|
||||
void (*deleteContextFunc_)(void*);
|
||||
};
|
||||
|
||||
#endif
|
||||
28
include/scenes/SceneWithDialogWidget.h
Executable file
28
include/scenes/SceneWithDialogWidget.h
Executable file
|
|
@ -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
|
||||
36
include/scenes/TestScene.h
Executable file
36
include/scenes/TestScene.h
Executable file
|
|
@ -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
|
||||
103
include/transferpak/TransferPakManager.h
Executable file
103
include/transferpak/TransferPakManager.h
Executable file
|
|
@ -0,0 +1,103 @@
|
|||
#ifndef _TRANSFERPAKMANAGER_H
|
||||
#define _TRANSFERPAKMANAGER_H
|
||||
|
||||
#include <libdragon.h>
|
||||
|
||||
#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
|
||||
64
include/transferpak/TransferPakRomReader.h
Executable file
64
include/transferpak/TransferPakRomReader.h
Executable file
|
|
@ -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
|
||||
62
include/transferpak/TransferPakSaveManager.h
Executable file
62
include/transferpak/TransferPakSaveManager.h
Executable file
|
|
@ -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
|
||||
69
include/widget/CursorWidget.h
Executable file
69
include/widget/CursorWidget.h
Executable file
|
|
@ -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
|
||||
118
include/widget/DialogWidget.h
Executable file
118
include/widget/DialogWidget.h
Executable file
|
|
@ -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
|
||||
49
include/widget/IFocusListener.h
Executable file
49
include/widget/IFocusListener.h
Executable file
|
|
@ -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
|
||||
75
include/widget/IWidget.h
Executable file
75
include/widget/IWidget.h
Executable file
|
|
@ -0,0 +1,75 @@
|
|||
#ifndef _IWIDGET_H
|
||||
#define _IWIDGET_H
|
||||
|
||||
#include "core/common.h"
|
||||
#include <libdragon.h>
|
||||
|
||||
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
|
||||
53
include/widget/ListItemFiller.h
Executable file
53
include/widget/ListItemFiller.h
Executable file
|
|
@ -0,0 +1,53 @@
|
|||
#ifndef _LISTITEMFILLER_H
|
||||
#define _LISTITEMFILLER_H
|
||||
|
||||
#include <vector>
|
||||
#include <libdragon.h>
|
||||
|
||||
/**
|
||||
* 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<typename ListType, typename ListDataType, typename ListItemWidgetType, typename ListItemWidgetStyleType>
|
||||
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<ListItemWidgetType*> widgets_;
|
||||
};
|
||||
|
||||
#endif
|
||||
123
include/widget/MenuItemWidget.h
Executable file
123
include/widget/MenuItemWidget.h
Executable file
|
|
@ -0,0 +1,123 @@
|
|||
#ifndef _MENUITEMWIDGET_H
|
||||
#define _MENUITEMWIDGET_H
|
||||
|
||||
#include "widget/IWidget.h"
|
||||
#include "core/Sprite.h"
|
||||
#include "core/RDPQGraphics.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
/**
|
||||
* 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
|
||||
148
include/widget/TransferPakDetectionWidget.h
Executable file
148
include/widget/TransferPakDetectionWidget.h
Executable file
|
|
@ -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
|
||||
162
include/widget/VerticalList.h
Executable file
162
include/widget/VerticalList.h
Executable file
|
|
@ -0,0 +1,162 @@
|
|||
#ifndef _VERTICALLIST_H
|
||||
#define _VERTICALLIST_H
|
||||
|
||||
#include "animations/IAnimation.h"
|
||||
#include "widget/IWidget.h"
|
||||
#include "core/Sprite.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
class RDPQGraphics;
|
||||
class IWidget;
|
||||
class VerticalList;
|
||||
class AnimationManager;
|
||||
|
||||
typedef std::vector<IWidget*> IWidgetList;
|
||||
typedef std::vector<Rectangle> WidgetBoundsList;
|
||||
|
||||
struct FocusChangeStatus;
|
||||
class IFocusListener;
|
||||
typedef std::vector<IFocusListener*> 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
|
||||
1
libdragon
Submodule
1
libdragon
Submodule
|
|
@ -0,0 +1 @@
|
|||
Subproject commit fd34d090adab1858815cf8e2c035ed4cd5e283a6
|
||||
1
libpokemegb
Submodule
1
libpokemegb
Submodule
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 747bdd4b4b27c8d1bd9c44f0d651d4c8c439b90c
|
||||
37
src/animations/AnimationManager.cpp
Executable file
37
src/animations/AnimationManager.cpp
Executable file
|
|
@ -0,0 +1,37 @@
|
|||
#include "animations/AnimationManager.h"
|
||||
#include "animations/IAnimation.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
static float normalizeTimeStep(IAnimation* anim, uint32_t elapsedTimeInMs)
|
||||
{
|
||||
return static_cast<float>(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
117
src/animations/IAnimation.cpp
Executable file
117
src/animations/IAnimation.cpp
Executable file
|
|
@ -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;
|
||||
}
|
||||
52
src/animations/MoveAnimation.cpp
Executable file
52
src/animations/MoveAnimation.cpp
Executable file
|
|
@ -0,0 +1,52 @@
|
|||
#include "animations/MoveAnimation.h"
|
||||
#include "widget/IWidget.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
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<int>(ceilf(pos * diffStartEnd_.x)),
|
||||
.y = startBounds_.y + static_cast<int>(ceilf(pos * diffStartEnd_.y)),
|
||||
.width = startBounds_.width + static_cast<int>(ceilf(pos * diffStartEnd_.width)),
|
||||
.height = startBounds_.height + static_cast<int>(ceilf(pos * diffStartEnd_.height))
|
||||
};
|
||||
|
||||
target_->setBounds(newBounds);
|
||||
}
|
||||
59
src/core/Application.cpp
Executable file
59
src/core/Application.cpp
Executable file
|
|
@ -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<uint32_t>(TICKS_TO_MS(after - before)));
|
||||
|
||||
graphics_.finishAndShowFrame();
|
||||
}
|
||||
}
|
||||
48
src/core/DragonUtils.cpp
Executable file
48
src/core/DragonUtils.cpp
Executable file
|
|
@ -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<int8_t>(abs(inputs.stick_x));
|
||||
const int8_t absYVal = static_cast<int8_t>(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;
|
||||
}
|
||||
36
src/core/FontManager.cpp
Executable file
36
src/core/FontManager.cpp
Executable file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
299
src/core/RDPQGraphics.cpp
Executable file
299
src/core/RDPQGraphics.cpp
Executable file
|
|
@ -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<float>(dstRect.width) / renderSettings.srcRect.width,
|
||||
.scale_y = static_cast<float>(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<float>(dstRect.width) / sprite->width,
|
||||
.scale_y = static_cast<float>(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<int16_t>(dstRect.width),
|
||||
.height = static_cast<int16_t>(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<int>(display_get_width()), .height = static_cast<int>(display_get_height()) });
|
||||
}
|
||||
16
src/core/common.cpp
Executable file
16
src/core/common.cpp
Executable file
|
|
@ -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};
|
||||
}
|
||||
9
src/main.cpp
Executable file
9
src/main.cpp
Executable file
|
|
@ -0,0 +1,9 @@
|
|||
#include "core/Application.h"
|
||||
|
||||
int main(void)
|
||||
{
|
||||
Application app;
|
||||
|
||||
app.init();
|
||||
app.run();
|
||||
}
|
||||
41
src/menu/MenuEntries.cpp
Executable file
41
src/menu/MenuEntries.cpp
Executable file
|
|
@ -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);
|
||||
120
src/menu/MenuFunctions.cpp
Executable file
120
src/menu/MenuFunctions.cpp
Executable file
|
|
@ -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<MenuScene*>(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<MenuScene*>(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<MenuScene*>(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<MenuScene*>(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<MenuScene*>(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);
|
||||
}
|
||||
97
src/scenes/AbstractUIScene.cpp
Executable file
97
src/scenes/AbstractUIScene.cpp
Executable file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
259
src/scenes/DistributionPokemonListScene.cpp
Executable file
259
src/scenes/DistributionPokemonListScene.cpp
Executable file
|
|
@ -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<DistributionPokemonListSceneContext*>(context);
|
||||
}
|
||||
|
||||
static void injectDistributionPokemon(void* context, const void* data)
|
||||
{
|
||||
auto scene = static_cast<DistributionPokemonListScene*>(context);
|
||||
scene->triggerPokemonInjection(data);
|
||||
}
|
||||
|
||||
DistributionPokemonListScene::DistributionPokemonListScene(SceneDependencies& deps, void* context)
|
||||
: MenuScene(deps, context)
|
||||
, romReader_(deps.tpakManager)
|
||||
, saveManager_(deps.tpakManager)
|
||||
, gen1Reader_(romReader_, saveManager_, static_cast<Gen1GameType>(deps.specificGenVersion))
|
||||
, gen2Reader_(romReader_, saveManager_, static_cast<Gen2GameType>(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<const Gen1DistributionPokemon*>(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<const Gen2DistributionPokemon*>(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<DistributionPokemonListSceneContext*>(context);
|
||||
delete toDelete;
|
||||
}
|
||||
5
src/scenes/IScene.cpp
Executable file
5
src/scenes/IScene.cpp
Executable file
|
|
@ -0,0 +1,5 @@
|
|||
#include "scenes/IScene.h"
|
||||
|
||||
IScene::~IScene()
|
||||
{
|
||||
}
|
||||
221
src/scenes/InitTransferPakScene.cpp
Executable file
221
src/scenes/InitTransferPakScene.cpp
Executable file
|
|
@ -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<uint32_t>(gen1MenuEntriesSize / sizeof(gen1MenuEntries[0]))
|
||||
});
|
||||
}
|
||||
else if(gen2Type != Gen2GameType::INVALID)
|
||||
{
|
||||
if(gen2Type == Gen2GameType::CRYSTAL)
|
||||
{
|
||||
menuContext = new MenuSceneContext({
|
||||
.menuEntries = gen2CrystalMenuEntries,
|
||||
.numMenuEntries = static_cast<uint32_t>(gen2CrystalMenuEntriesSize / sizeof(gen2CrystalMenuEntries[0]))
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
menuContext = new MenuSceneContext({
|
||||
.menuEntries = gen2MenuEntries,
|
||||
.numMenuEntries = static_cast<uint32_t>(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<int>(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<uint8_t>(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<uint8_t>(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';
|
||||
}
|
||||
229
src/scenes/MenuScene.cpp
Executable file
229
src/scenes/MenuScene.cpp
Executable file
|
|
@ -0,0 +1,229 @@
|
|||
#include "scenes/MenuScene.h"
|
||||
#include "core/FontManager.h"
|
||||
#include "scenes/SceneManager.h"
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
static void dialogFinishedCallback(void* context)
|
||||
{
|
||||
MenuScene* scene = (MenuScene*)context;
|
||||
scene->onDialogDone();
|
||||
}
|
||||
|
||||
MenuScene::MenuScene(SceneDependencies& deps, void* context)
|
||||
: SceneWithDialogWidget(deps)
|
||||
, context_(static_cast<MenuSceneContext*>(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<MenuSceneContext*>(context);
|
||||
delete menuContext;
|
||||
}
|
||||
144
src/scenes/SceneManager.cpp
Executable file
144
src/scenes/SceneManager.cpp
Executable file
|
|
@ -0,0 +1,144 @@
|
|||
#include "scenes/SceneManager.h"
|
||||
#include "scenes/TestScene.h"
|
||||
#include "scenes/InitTransferPakScene.h"
|
||||
#include "scenes/DistributionPokemonListScene.h"
|
||||
|
||||
#include <libdragon.h>
|
||||
|
||||
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;
|
||||
}
|
||||
67
src/scenes/SceneWithDialogWidget.cpp
Executable file
67
src/scenes/SceneWithDialogWidget.cpp
Executable file
|
|
@ -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});
|
||||
}
|
||||
93
src/scenes/TestScene.cpp
Executable file
93
src/scenes/TestScene.cpp
Executable file
|
|
@ -0,0 +1,93 @@
|
|||
#include "scenes/TestScene.h"
|
||||
#include "core/RDPQGraphics.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <n64sys.h>
|
||||
|
||||
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_);
|
||||
}
|
||||
299
src/transferpak/TransferPakManager.cpp
Executable file
299
src/transferpak/TransferPakManager.cpp
Executable file
|
|
@ -0,0 +1,299 @@
|
|||
#include "transferpak/TransferPakManager.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <unistd.h>
|
||||
|
||||
/** @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<int>(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<int>(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<int>(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<uint16_t>(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<uint16_t>(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;
|
||||
}
|
||||
98
src/transferpak/TransferPakRomReader.cpp
Executable file
98
src/transferpak/TransferPakRomReader.cpp
Executable file
|
|
@ -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<uint16_t>(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<uint16_t>(romOffset);
|
||||
}
|
||||
else
|
||||
{
|
||||
return static_cast<uint16_t>(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<uint16_t>(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<uint8_t>(currentRomOffset_ / GB_BANK_SIZE);
|
||||
}
|
||||
118
src/transferpak/TransferPakSaveManager.cpp
Executable file
118
src/transferpak/TransferPakSaveManager.cpp
Executable file
|
|
@ -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<uint16_t>(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<uint16_t>(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<uint16_t>(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<uint8_t>(sramOffset_ / GB_SRAM_BANK_SIZE);
|
||||
}
|
||||
130
src/widget/CursorWidget.cpp
Executable file
130
src/widget/CursorWidget.cpp
Executable file
|
|
@ -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);
|
||||
}
|
||||
192
src/widget/DialogWidget.cpp
Executable file
192
src/widget/DialogWidget.cpp
Executable file
|
|
@ -0,0 +1,192 @@
|
|||
#include "widget/DialogWidget.h"
|
||||
#include "core/RDPQGraphics.h"
|
||||
|
||||
#include <cstdarg>
|
||||
|
||||
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);
|
||||
}
|
||||
5
src/widget/IFocusListener.cpp
Executable file
5
src/widget/IFocusListener.cpp
Executable file
|
|
@ -0,0 +1,5 @@
|
|||
#include "widget/IFocusListener.h"
|
||||
|
||||
IFocusListener::~IFocusListener()
|
||||
{
|
||||
}
|
||||
110
src/widget/MenuItemWidget.cpp
Executable file
110
src/widget/MenuItemWidget.cpp
Executable file
|
|
@ -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);
|
||||
}
|
||||
241
src/widget/TransferPakDetectionWidget.cpp
Executable file
241
src/widget/TransferPakDetectionWidget.cpp
Executable file
|
|
@ -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);
|
||||
}
|
||||
439
src/widget/VerticalList.cpp
Executable file
439
src/widget/VerticalList.cpp
Executable file
|
|
@ -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 <cstddef>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
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<int>(listHeight));
|
||||
}
|
||||
|
||||
static int32_t getVerticalWindowScrollNeededToMakeWidgetFullyVisible(const Rectangle& widgetBounds, uint32_t listHeight, uint32_t windowStartY)
|
||||
{
|
||||
if(widgetBounds.y < static_cast<int32_t>(windowStartY))
|
||||
{
|
||||
return widgetBounds.y - static_cast<int32_t>(windowStartY);
|
||||
}
|
||||
|
||||
const int32_t widgetEndY = widgetBounds.y + widgetBounds.height;
|
||||
const int32_t listEndY = static_cast<int32_t>(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<int>(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<int32_t>(windowStartY);
|
||||
windowEndY_ = static_cast<int32_t>(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<uint32_t>(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<uint32_t>(widgetList_.size()), static_cast<uint32_t>(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);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user