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:
Philippe Symons 2024-07-19 21:46:11 +02:00
parent 03e87d734b
commit e51726eef9
70 changed files with 5770 additions and 197 deletions

6
.gitmodules vendored Normal file
View 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
View 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
View File

@ -1,201 +1,25 @@
Apache License Copyright © 2024 PokeMe64. All rights reserved.
Version 2.0, January 2004
http://www.apache.org/licenses/
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, * Redistributions in binary form must reproduce the above copyright
and distribution as defined by Sections 1 through 9 of this document. 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 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
the copyright owner that is granting the License. "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
"Legal Entity" shall mean the union of the acting entity and all A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
other entities that control, are controlled by, or are under common HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
control with that entity. For the purposes of this definition, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
"control" means (i) the power, direct or indirect, to cause the LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
direction or management of such entity, whether by contract or DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
otherwise, or (ii) ownership of fifty percent (50%) or more of the THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
outstanding shares, or (iii) beneficial ownership of such entity. (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"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.

57
Makefile Executable file
View 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
View 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

Binary file not shown.

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 359 B

BIN
assets/oak.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

BIN
assets/pokeball.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 600 B

View 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
View 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

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

View 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

View 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
View 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

View 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
View 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
View 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

View 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
View 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

View 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

View 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

View 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
View 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
View 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
View 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
View 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
View 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
View 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

View 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
View 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

@ -0,0 +1 @@
Subproject commit fd34d090adab1858815cf8e2c035ed4cd5e283a6

1
libpokemegb Submodule

@ -0,0 +1 @@
Subproject commit 747bdd4b4b27c8d1bd9c44f0d651d4c8c439b90c

View 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
View 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;
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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);
}
}

View 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
View File

@ -0,0 +1,5 @@
#include "scenes/IScene.h"
IScene::~IScene()
{
}

View 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
View 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
View 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;
}

View 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
View 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_);
}

View 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;
}

View 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);
}

View 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
View 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
View 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
View File

@ -0,0 +1,5 @@
#include "widget/IFocusListener.h"
IFocusListener::~IFocusListener()
{
}

110
src/widget/MenuItemWidget.cpp Executable file
View 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);
}

View 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
View 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);
}
}