From cf3cb7a219920cf68b40e2797c0c7716fdc2a904 Mon Sep 17 00:00:00 2001 From: J-D-K Date: Fri, 6 Dec 2024 20:44:47 -0500 Subject: [PATCH] Data partially implemented with no crashes. Getting too tired. --- Include/AppStates/AppState.hpp | 8 +- Include/AppStates/MainMenuState.hpp | 23 +++ Include/Colors.hpp | 1 + Include/Config.hpp | 52 +++++++ Include/Data/AccountUID.hpp | 12 ++ Include/Data/Data.hpp | 3 + Include/Data/TitleInfo.hpp | 28 ++++ Include/Data/User.hpp | 41 ++++++ Include/StringUtil.hpp | 7 +- Include/Strings.hpp | 2 + Include/UI/ColorMod.hpp | 23 +++ Include/UI/Element.hpp | 15 ++ Include/UI/IconMenu.hpp | 14 ++ Include/UI/Menu.hpp | 53 +++++++ Include/UI/RenderFunctions.hpp | 2 +- Makefile | 2 +- RomFS/Text/ENUS.json | 15 ++ RomFS/Textures/MenuBackground.png | Bin 0 -> 67177 bytes Source/AppStates/MainMenuState.cpp | 28 ++++ Source/Config.cpp | 217 ++++++++++++++++++++++++++++ Source/Data/Data.cpp | 120 +++++++++++++++ Source/Data/TitleInfo.cpp | 63 ++++++++ Source/Data/User.cpp | 117 +++++++++++++++ Source/JKSV.cpp | 55 ++++++- Source/StringUtil.cpp | 64 +++++++- Source/Strings.cpp | 30 ++++ Source/UI/ColorMod.cpp | 18 +++ Source/UI/Menu.cpp | 124 ++++++++++++++++ Source/UI/RenderFunctions.cpp | 26 ++++ 29 files changed, 1150 insertions(+), 13 deletions(-) create mode 100644 Include/AppStates/MainMenuState.hpp create mode 100644 Include/Config.hpp create mode 100644 Include/Data/AccountUID.hpp create mode 100644 Include/Data/TitleInfo.hpp create mode 100644 Include/Data/User.hpp create mode 100644 Include/UI/ColorMod.hpp create mode 100644 Include/UI/Element.hpp create mode 100644 Include/UI/IconMenu.hpp create mode 100644 Include/UI/Menu.hpp create mode 100644 RomFS/Textures/MenuBackground.png create mode 100644 Source/AppStates/MainMenuState.cpp create mode 100644 Source/Config.cpp create mode 100644 Source/Data/Data.cpp create mode 100644 Source/Data/TitleInfo.cpp create mode 100644 Source/Data/User.cpp create mode 100644 Source/UI/ColorMod.cpp create mode 100644 Source/UI/Menu.cpp diff --git a/Include/AppStates/AppState.hpp b/Include/AppStates/AppState.hpp index 93c7fc3..234f962 100644 --- a/Include/AppStates/AppState.hpp +++ b/Include/AppStates/AppState.hpp @@ -6,11 +6,17 @@ class AppState { public: AppState(void) = default; - virtual ~AppState() = 0; + virtual ~AppState() {}; virtual void Update(void) = 0; virtual void Render(void) = 0; + // Allows the state to be reactivated if needed. + void Reactivate(void) + { + m_IsActive = true; + } + // Returns whether or not state is still active or can be purged. bool IsActive(void) const { diff --git a/Include/AppStates/MainMenuState.hpp b/Include/AppStates/MainMenuState.hpp new file mode 100644 index 0000000..4ba7af2 --- /dev/null +++ b/Include/AppStates/MainMenuState.hpp @@ -0,0 +1,23 @@ +#pragma once +#include "AppStates/AppState.hpp" +#include "SDL.hpp" + +class MainMenuState : public AppState +{ + public: + MainMenuState(void); + ~MainMenuState() {}; + + void Update(void); + void Render(void); + + private: + // The render target. + SDL::SharedTexture m_RenderTarget = nullptr; + // Background of the menu. + SDL::SharedTexture m_Background = nullptr; + // Pointer to control guide string so I don't need to call and fetch it every loop. + const char *m_ControlGuide = nullptr; + // X coordinate to render controls at. + int m_ControlGuideX; +}; diff --git a/Include/Colors.hpp b/Include/Colors.hpp index f36b510..9227edd 100644 --- a/Include/Colors.hpp +++ b/Include/Colors.hpp @@ -13,4 +13,5 @@ namespace Colors static constexpr SDL::Color ClearColor = {0x2D2D2DFF}; static constexpr SDL::Color DialogBox = {0x505050FF}; static constexpr SDL::Color BackgroundDim = {0x00000088}; + static constexpr SDL::Color Transparent = {0x00000000}; } // namespace Colors diff --git a/Include/Config.hpp b/Include/Config.hpp new file mode 100644 index 0000000..3d66559 --- /dev/null +++ b/Include/Config.hpp @@ -0,0 +1,52 @@ +#pragma once +#include "FsLib.hpp" +#include + +namespace Config +{ + // Attempts to load config from file. If that fails, initializes to default. + void Initialize(void); + // Resets config to default settings. + void ResetToDefault(void); + // Saves config. + void Save(void); + // Retrieves a value with its key. + uint8_t GetByKey(std::string_view Key); + // Gets the working directory. + FsLib::Path GetWorkingDirectory(void); + // Gets the UI's transition scaling + double GetAnimationScaling(void); + + // Adds or removes title from favorites. + void AddRemoveFavorite(uint64_t TitleID); + // Returns if TitleID is in favorites list. + bool IsFavorite(uint64_t TitleID); + + // Adds or removes a title from the blacklist + void AddRemoveBlacklist(uint64_t TitleID); + // Returns if TitleID is in blacklist + bool IsBlacklisted(uint64_t TitleID); + + // Names of keys. Note: Not all of these are retrievable with GetByKey. Some of these are purely for config reading and writing. + namespace Keys + { + static constexpr std::string_view WorkingDirectory = "WorkingDirectory"; + static constexpr std::string_view IncludeDeviceSaves = "IncludeDeviceSaves"; + static constexpr std::string_view AutoBackupOnRestore = "AutoBackupOnRestore"; + static constexpr std::string_view HoldForDeletion = "HoldForDeletion"; + static constexpr std::string_view HoldForRestoration = "HoldForRestoration"; + static constexpr std::string_view HoldForOverwrite = "HoldForOverWrite"; + static constexpr std::string_view OnlyListMountable = "OnlyListMountable"; + static constexpr std::string_view ListAccountSystemSaves = "ListAccountSystemSaves"; + static constexpr std::string_view AllowSystemSaveWriting = "AllowSystemSaveWriting"; + static constexpr std::string_view ExportToZip = "ExportToZip"; + static constexpr std::string_view ForceEnglish = "ForceEnglish"; + static constexpr std::string_view EnableTrashBin = "EnableTrash"; + static constexpr std::string_view AutoNameBackups = "AutoNameBackups"; + static constexpr std::string_view TitleSortType = "TitleSortType"; + static constexpr std::string_view Favorites = "Favorites"; + static constexpr std::string_view BlackList = "BlackList"; + static constexpr std::string_view AutoUpload = "AutoUploadToRemote"; + static constexpr std::string_view UIAnimationScaling = "UIAnimationScaling"; + } // namespace Keys +} // namespace Config diff --git a/Include/Data/AccountUID.hpp b/Include/Data/AccountUID.hpp new file mode 100644 index 0000000..ff26e35 --- /dev/null +++ b/Include/Data/AccountUID.hpp @@ -0,0 +1,12 @@ +#pragma once +#include + +static inline bool operator==(AccountUid AccountID1, AccountUid AccountID2) +{ + return (AccountID1.uid[0] == AccountID2.uid[0]) && (AccountID1.uid[1] == AccountID2.uid[1]); +} + +static inline u128 AccountUIDToU128(AccountUid AccountID) +{ + return (static_cast(AccountID.uid[0]) << 64 | AccountID.uid[1]); +} diff --git a/Include/Data/Data.hpp b/Include/Data/Data.hpp index a241425..03215ab 100644 --- a/Include/Data/Data.hpp +++ b/Include/Data/Data.hpp @@ -1,4 +1,7 @@ #pragma once +#include "Data/AccountUID.hpp" +#include "Data/TitleInfo.hpp" +#include "Data/User.hpp" namespace Data { diff --git a/Include/Data/TitleInfo.hpp b/Include/Data/TitleInfo.hpp new file mode 100644 index 0000000..f9f3aeb --- /dev/null +++ b/Include/Data/TitleInfo.hpp @@ -0,0 +1,28 @@ +#pragma once +#include "SDL.hpp" +#include +#include + +namespace Data +{ + class TitleInfo + { + public: + // Loads control data and icon. + TitleInfo(uint64_t ApplicationID); + + // Returns title. + const char *GetTitle(void); + // Returns publisher + const char *GetPublisher(void); + + // Returns icon + SDL::SharedTexture GetIcon(void) const; + + private: + // This is where all the important stuff is. + NacpStruct m_NACP; + // This is the icon. + SDL::SharedTexture m_Icon = nullptr; + }; +} // namespace Data diff --git a/Include/Data/User.hpp b/Include/Data/User.hpp new file mode 100644 index 0000000..2a29190 --- /dev/null +++ b/Include/Data/User.hpp @@ -0,0 +1,41 @@ +#pragma once +#include "SDL.hpp" +#include +#include +#include +#include + +namespace Data +{ + class User + { + public: + // This is for normal use accounts. + User(AccountUid AccountID); + // This is for system type accounts. + User(AccountUid AccountID, std::string_view Nickname, std::string_view PathSafeNickname); + + // Adds Data to UserDataMap + void AddToMap(const FsSaveDataInfo &SaveInfo, const PdmPlayStatistics &PlayStats); + // Returns FsSaveDataInfo by ApplicationID or SystemSaveID. + FsSaveDataInfo *GetSaveInfoByID(uint64_t ApplicationID); + // Returns PlayStats according to ^ + PdmPlayStatistics *GetPlayStatsByID(uint64_t ApplicationID); + + private: + // User's ID. + AccountUid m_AccountID; + // Nickname + char m_Nickname[0x20]; + // Path safe nickname. + char m_PathSafeNickname[0x20]; + // User's icon + SDL::SharedTexture m_Icon = nullptr; + // Map of FsSaveInfo and play stats + std::unordered_map> m_UserDataMap; + // Loads account using profile structs. + void LoadAccount(AccountProfile &Profile, AccountProfileBase &ProfileBase); + // Creates a placeholder since something went wrong getting profile info. + void CreateAccount(void); + }; +} // namespace Data diff --git a/Include/StringUtil.hpp b/Include/StringUtil.hpp index 8b85e01..8e8ab79 100644 --- a/Include/StringUtil.hpp +++ b/Include/StringUtil.hpp @@ -3,5 +3,10 @@ namespace StringUtil { + // Gets a string formatted with va args. std::string GetFormattedString(const char *Format, ...); -} + // Replaces a sequence of characters in a string with another. + void ReplaceInString(std::string &Target, std::string_view Find, std::string_view Replace); + // Tries to make string path safe. Returns false if it's not possible. + bool SanitizeStringForPath(const char *StringIn, char *StringOut, size_t StringOutSize); +} // namespace StringUtil diff --git a/Include/Strings.hpp b/Include/Strings.hpp index d723a81..3be110d 100644 --- a/Include/Strings.hpp +++ b/Include/Strings.hpp @@ -11,5 +11,7 @@ namespace Strings namespace Names { static constexpr std::string_view TranslationInfo = "TranslationInfo"; + static constexpr std::string_view ControlGuides = "ControlGuides"; + static constexpr std::string_view SaveDataTypes = "SaveDataTypes"; } // namespace Names } // namespace Strings diff --git a/Include/UI/ColorMod.hpp b/Include/UI/ColorMod.hpp new file mode 100644 index 0000000..b9847d2 --- /dev/null +++ b/Include/UI/ColorMod.hpp @@ -0,0 +1,23 @@ +#pragma once +#include + +namespace UI +{ + // I got tired of repeating this for every class that needs to render a bounding box + class ColorMod + { + public: + ColorMod(void) = default; + + // Updates the color mod. + void Update(void); + // Allows me to use this like the class isn't even there. + operator uint8_t(void) const; + + private: + // Which direction the shift is going. True = add, false = subtract; + bool m_Direction = true; + // Current mod value. + uint8_t m_ColorMod = 0; + }; +} // namespace UI diff --git a/Include/UI/Element.hpp b/Include/UI/Element.hpp new file mode 100644 index 0000000..6ae338d --- /dev/null +++ b/Include/UI/Element.hpp @@ -0,0 +1,15 @@ +#pragma once +#include "SDL.hpp" + +namespace UI +{ + class Element + { + public: + Element(void) = default; + virtual ~Element() {}; + + virtual void Update(void) = 0; + virtual void Render(SDL_Texture *Target); + }; +} // namespace UI diff --git a/Include/UI/IconMenu.hpp b/Include/UI/IconMenu.hpp new file mode 100644 index 0000000..5464c49 --- /dev/null +++ b/Include/UI/IconMenu.hpp @@ -0,0 +1,14 @@ +#pragma once +#include "UI/Menu.hpp" + +namespace UI +{ + class IconMenu : public UI::Menu + { + public: + IconMenu(int X, int Y, int ScrollLength); + + private: + std::vector m_Options; + }; +} // namespace UI diff --git a/Include/UI/Menu.hpp b/Include/UI/Menu.hpp new file mode 100644 index 0000000..f405892 --- /dev/null +++ b/Include/UI/Menu.hpp @@ -0,0 +1,53 @@ +#pragma once +#include "SDL.hpp" +#include "UI/ColorMod.hpp" +#include "UI/Element.hpp" +#include +#include + +namespace UI +{ + class Menu : public UI::Element + { + public: + // Coordinates to render to and dimensions of menu. + Menu(int X, int Y, int Width, int FontSize, int ScrollLength); + ~Menu() {}; + + // Required functions from Element. + void Update(void); + void Render(SDL_Texture *Target); + + // Adds an option to the menu. + void AddOption(std::string_view NewOption); + + // Returns the index of the selected item. + int GetSelected(void) const; + // Sets the selected option + void SetSelected(int Selected); + // Resets the menu. + void Reset(void); + + private: + // X and Y coordinates. + double m_X, m_Y; + // These are used for the scrolling effect. + double m_OriginalY, m_TargetY; + // Font size + int m_FontSize; + // How many options before scrolling starts happening. + int m_ScrollLength, m_MenuRenderLength; + // Width. Height is calculated on construction. + int m_Width, m_Height; + // Selected option. + int m_Selected = 0; + // Actual length of the menu. + int m_OptionsLength = -1; + // Color mod for rendering bounding box. + UI::ColorMod m_ColorMod; + // A small target to render the option to so it can't draw text outside of the bounding area. + SDL::SharedTexture m_OptionTarget = nullptr; + // Vector of options. + std::vector m_Options; + }; +} // namespace UI diff --git a/Include/UI/RenderFunctions.hpp b/Include/UI/RenderFunctions.hpp index 8a5cb3b..541918c 100644 --- a/Include/UI/RenderFunctions.hpp +++ b/Include/UI/RenderFunctions.hpp @@ -5,5 +5,5 @@ namespace UI { void RenderDialogBox(SDL_Texture *Target, int X, int Y, int Width, int Height); - void RenderBoundingBox(SDL_Texture *Target, int X, int Y, int Width, int Height); + void RenderBoundingBox(SDL_Texture *Target, int X, int Y, int Width, int Height, uint8_t ColorMod); } // namespace UI diff --git a/Makefile b/Makefile index 598383b..b1a6c5f 100644 --- a/Makefile +++ b/Makefile @@ -32,7 +32,7 @@ include $(DEVKITPRO)/libnx/switch_rules #--------------------------------------------------------------------------------- TARGET := JKSV BUILD := build -SOURCES := Source Source/AppStates Source/UI +SOURCES := Source Source/AppStates Source/UI Source/Data DATA := data INCLUDES := Include ./Libraries/FsLib/Switch/FsLib/include ./Libraries/SDLLib/SDL/include EXEFS_SRC := exefs_src diff --git a/RomFS/Text/ENUS.json b/RomFS/Text/ENUS.json index 81f108a..baf978b 100644 --- a/RomFS/Text/ENUS.json +++ b/RomFS/Text/ENUS.json @@ -2,5 +2,20 @@ "TranslationInfo" : [ "Translated By: %s", "NULL" + ], + "ControlGuides" : [ + "[A] Select [Y] Dump All Saves [X] User Options", + "[A] Select [L][R] Jump [Y] Favorite [X] Title Options [B] Back", + "[A] Select [Y] Restore [X] Delete [ZR] Upload [B] Close", + "[A] Toggle [X] Defaults [B] Back" + ], + "SaveDataTypes" : [ + "System", + "Account", + "BCAT", + "Device", + "Temporary", + "Cache", + "System BCAT" ] } diff --git a/RomFS/Textures/MenuBackground.png b/RomFS/Textures/MenuBackground.png new file mode 100644 index 0000000000000000000000000000000000000000..d77ab13d059e2804b032d07994bc148563da8d09 GIT binary patch literal 67177 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%0A-Y6@F;DRALpKsg^CWvRf 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�{j>X{DbN3Kwr3$z0J_eqj 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, 0, 0); + + // Render target to screen. + m_RenderTarget->Render(NULL, 0, 91); + + // Control Guide. + if (AppState::HasFocus()) + { + SDL::Text::Render(NULL, m_ControlGuideX, 673, 20, SDL::Text::NO_TEXT_WRAP, Colors::White, m_ControlGuide); + } +} diff --git a/Source/Config.cpp b/Source/Config.cpp new file mode 100644 index 0000000..b73bb55 --- /dev/null +++ b/Source/Config.cpp @@ -0,0 +1,217 @@ +#include "Config.hpp" +#include "JSON.hpp" +#include "Logger.hpp" +#include +#include +#include +#include +#include +#include + +namespace +{ + // Config path(s) + const char *CONFIG_FOLDER = "sdmc:/config/JKSV"; + const char *CONFIG_PATH = "sdmc:/config/JKSV/JKSV.json"; + // Map of config values + std::unordered_map s_ConfigMap; + // Working directory + FsLib::Path s_WorkingDirectory; + // UI animation scaling. + double s_UIAnimationScaling; + // Vector of favorite title ids + std::vector s_Favorites; + // Vector of titles to ignore. + std::vector s_Blacklist; +} // namespace + +static void ReadArrayToVector(std::vector &Vector, json_object *Array) +{ + // Just in case. Shouldn't happen though. + Vector.clear(); + + size_t ArrayLength = json_object_array_length(Array); + for (size_t i = 0; i < ArrayLength; i++) + { + json_object *ArrayEntry = json_object_array_get_idx(Array, i); + if (!ArrayEntry) + { + continue; + } + Vector.push_back(json_object_get_uint64(ArrayEntry)); + } +} + +void Config::Initialize(void) +{ + if (!FsLib::DirectoryExists(CONFIG_FOLDER) && !FsLib::CreateDirectoriesRecursively(CONFIG_FOLDER)) + { + Logger::Log("Error creating config folder: %s.", FsLib::GetErrorString()); + Config::ResetToDefault(); + return; + } + + JSON::Object ConfigJSON = JSON::NewObject(json_object_from_file, CONFIG_PATH); + if (!ConfigJSON) + { + Logger::Log("Error opening config for reading: %s", FsLib::GetErrorString()); + Config::ResetToDefault(); + return; + } + + json_object_iterator ConfigIterator = json_object_iter_begin(ConfigJSON.get()); + json_object_iterator ConfigEnd = json_object_iter_end(ConfigJSON.get()); + while (!json_object_iter_equal(&ConfigIterator, &ConfigEnd)) + { + const char *KeyName = json_object_iter_peek_name(&ConfigIterator); + json_object *ConfigValue = json_object_iter_peek_value(&ConfigIterator); + + // These are exemptions. + if (std::strcmp(KeyName, Config::Keys::WorkingDirectory.data()) == 0) + { + s_WorkingDirectory = json_object_get_string(ConfigValue); + } + else if (std::strcmp(KeyName, Config::Keys::UIAnimationScaling.data()) == 0) + { + s_UIAnimationScaling = json_object_get_double(ConfigValue); + } + else if (std::strcmp(KeyName, Config::Keys::Favorites.data()) == 0) + { + ReadArrayToVector(s_Favorites, ConfigValue); + } + else if (std::strcmp(KeyName, Config::Keys::BlackList.data()) == 0) + { + ReadArrayToVector(s_Blacklist, ConfigValue); + } + else + { + s_ConfigMap[KeyName] = json_object_get_uint64(ConfigValue); + } + json_object_iter_next(&ConfigIterator); + } +} + +void Config::ResetToDefault(void) +{ + s_WorkingDirectory = "sdmc:/JKSV"; + s_ConfigMap[Config::Keys::IncludeDeviceSaves.data()] = 1; + s_ConfigMap[Config::Keys::AutoBackupOnRestore.data()] = 1; + s_ConfigMap[Config::Keys::HoldForDeletion.data()] = 1; + s_ConfigMap[Config::Keys::HoldForRestoration.data()] = 1; + s_ConfigMap[Config::Keys::HoldForOverwrite.data()] = 1; + s_ConfigMap[Config::Keys::OnlyListMountable.data()] = 1; + s_ConfigMap[Config::Keys::ListAccountSystemSaves.data()] = 0; + s_ConfigMap[Config::Keys::AllowSystemSaveWriting.data()] = 0; + s_ConfigMap[Config::Keys::ExportToZip.data()] = 1; + s_ConfigMap[Config::Keys::ForceEnglish.data()] = 0; + s_ConfigMap[Config::Keys::EnableTrashBin.data()] = 1; + s_ConfigMap[Config::Keys::AutoNameBackups.data()] = 0; + s_ConfigMap[Config::Keys::TitleSortType.data()] = 0; + s_ConfigMap[Config::Keys::AutoUpload.data()] = 0; + s_UIAnimationScaling = 2.5f; +} + +void Config::Save(void) +{ + JSON::Object ConfigJSON = JSON::NewObject(json_object_new_object); + + // Add working directory first. + json_object *WorkingDirectory = json_object_new_string(s_WorkingDirectory.CString()); + json_object_object_add(ConfigJSON.get(), Config::Keys::WorkingDirectory.data(), WorkingDirectory); + + // Loop through map and add it. + for (auto &[Key, Value] : s_ConfigMap) + { + json_object *JsonValue = json_object_new_uint64(Value); + json_object_object_add(ConfigJSON.get(), Key.c_str(), JsonValue); + } + + // Add UI scaling. + json_object *Scaling = json_object_new_double(s_UIAnimationScaling); + json_object_object_add(ConfigJSON.get(), Config::Keys::UIAnimationScaling.data(), Scaling); + + // Favorites + json_object *FavoritesArray = json_object_new_array(); + for (uint64_t &TitleID : s_Favorites) + { + json_object *NewFavorite = json_object_new_uint64(TitleID); + json_object_array_add(FavoritesArray, NewFavorite); + } + json_object_object_add(ConfigJSON.get(), Config::Keys::Favorites.data(), FavoritesArray); + + // Same but blacklist + json_object *BlacklistArray = json_object_new_array(); + for (uint64_t &TitleID : s_Blacklist) + { + json_object *NewBlacklist = json_object_new_uint64(TitleID); + json_object_array_add(BlacklistArray, NewBlacklist); + } + json_object_object_add(ConfigJSON.get(), Config::Keys::BlackList.data(), BlacklistArray); + + // Write config file + FsLib::File ConfigFile(CONFIG_PATH, FsOpenMode_Create | FsOpenMode_Write, std::strlen(json_object_get_string(ConfigJSON.get()))); + ConfigFile << json_object_get_string(ConfigJSON.get()); +} + +uint8_t Config::GetByKey(std::string_view Key) +{ + if (s_ConfigMap.find(Key.data()) == s_ConfigMap.end()) + { + return 0; + } + return s_ConfigMap.at(Key.data()); +} + +FsLib::Path Config::GetWorkingDirectory(void) +{ + return s_WorkingDirectory; +} + +double Config::GetAnimationScaling(void) +{ + return s_UIAnimationScaling; +} + +void Config::AddRemoveFavorite(uint64_t TitleID) +{ + auto FindTitle = std::find(s_Favorites.begin(), s_Favorites.end(), TitleID); + if (FindTitle == s_Favorites.end()) + { + s_Favorites.push_back(TitleID); + } + else + { + s_Favorites.erase(FindTitle); + } +} + +bool Config::IsFavorite(uint64_t TitleID) +{ + if (std::find(s_Favorites.begin(), s_Favorites.end(), TitleID) == s_Favorites.end()) + { + return false; + } + return true; +} + +void Config::AddRemoveBlacklist(uint64_t TitleID) +{ + auto FindTitle = std::find(s_Blacklist.begin(), s_Blacklist.end(), TitleID); + if (FindTitle == s_Blacklist.end()) + { + s_Blacklist.push_back(TitleID); + } + else + { + s_Blacklist.erase(FindTitle); + } +} + +bool Config::IsBlacklisted(uint64_t TitleID) +{ + if (std::find(s_Blacklist.begin(), s_Blacklist.end(), TitleID) == s_Blacklist.end()) + { + return false; + } + return true; +} diff --git a/Source/Data/Data.cpp b/Source/Data/Data.cpp new file mode 100644 index 0000000..867930b --- /dev/null +++ b/Source/Data/Data.cpp @@ -0,0 +1,120 @@ +#include "Data/Data.hpp" +#include "Config.hpp" +#include "Data/AccountUID.hpp" +#include "FsLib.hpp" +#include "Logger.hpp" +#include "Strings.hpp" +#include +#include +#include +#include +#include + +namespace +{ + // This is easer to read imo + using UserIDPair = std::pair; + // User vector to preserve order. + std::vector s_UserVector; + // Map of Title info paired with its title/application + std::unordered_map s_TitleInfoMap; + // Array of SaveDataSpaceIDs - SaveDataSpaceAll doesn't seem to work as it should... + constexpr std::array s_SaveDataSpaceOrder = {FsSaveDataSpaceId_System, + FsSaveDataSpaceId_User, + FsSaveDataSpaceId_SdSystem, + FsSaveDataSpaceId_Temporary, + FsSaveDataSpaceId_SdUser, + FsSaveDataSpaceId_ProperSystem, + FsSaveDataSpaceId_SafeMode}; + +} // namespace + +bool Data::Initialize(void) +{ + // Switch can only have up to 8 accounts. + int TotalAccountsRead = 0; + AccountUid AccountIDs[8]; + if (R_FAILED(accountListAllUsers(AccountIDs, 8, &TotalAccountsRead))) + { + Logger::Log("Error getting user list: 0x%X.", TotalAccountsRead); + return false; + } + + // Loop through and load all users found. + for (int i = 0; i < TotalAccountsRead; i++) + { + Data::User NewUser(AccountIDs[i]); + s_UserVector.push_back(std::make_pair(AccountIDs[i], std::move(NewUser))); + } + + // "System" users. + AccountUid DeviceID = {FsSaveDataType_Device}; + AccountUid BCATID = {FsSaveDataType_Bcat}; + AccountUid CacheID = {FsSaveDataType_Cache}; + AccountUid SystemID = {FsSaveDataType_System}; + + s_UserVector.push_back( + std::make_pair(DeviceID, Data::User({FsSaveDataType_Device}, Strings::GetByName(Strings::Names::SaveDataTypes, 3), "Device"))); + s_UserVector.push_back( + std::make_pair(BCATID, Data::User({FsSaveDataType_Bcat}, Strings::GetByName(Strings::Names::SaveDataTypes, 2), "BCAT"))); + s_UserVector.push_back( + std::make_pair(CacheID, Data::User({FsSaveDataType_Cache}, Strings::GetByName(Strings::Names::SaveDataTypes, 5), "Cache"))); + s_UserVector.push_back( + std::make_pair(SystemID, Data::User({FsSaveDataType_System}, Strings::GetByName(Strings::Names::SaveDataTypes, 0), "System"))); + + NsApplicationRecord CurrentRecord = {0}; + int EntryCount = 0, EntryOffset = 0; + while (R_SUCCEEDED(nsListApplicationRecord(&CurrentRecord, 1, EntryOffset++, &EntryCount)) && EntryCount > 0) + { + s_TitleInfoMap.emplace(std::make_pair(CurrentRecord.application_id, Data::TitleInfo(CurrentRecord.application_id))); + } + + for (int i = 0; i < 7; i++) + { + FsSaveDataInfo SaveInfo; + FsSaveDataInfoReader SaveInfoReader; + int64_t TotalEntries = 0; + + Result FsError = fsOpenSaveDataInfoReader(&SaveInfoReader, s_SaveDataSpaceOrder[i]); + if (R_FAILED(FsError)) + { + Logger::Log("Error opening save data reader with space ID %i.", i); + continue; + } + + while (R_SUCCEEDED(fsSaveDataInfoReaderRead(&SaveInfoReader, &SaveInfo, 1, &TotalEntries)) && TotalEntries > 0) + { + auto FindUser = std::find_if(s_UserVector.begin(), s_UserVector.end(), [&SaveInfo](UserIDPair &IDPair) { + return IDPair.first == SaveInfo.uid; + }); + + // To do: Handle this right. + if (FindUser == s_UserVector.end()) + { + continue; + } + + // This is for if we have system save data. It has no application ID. + uint64_t ApplicationID = (SaveInfo.save_data_type == FsSaveDataType_System || SaveInfo.save_data_type == FsSaveDataType_SystemBcat) + ? SaveInfo.system_save_data_id + : SaveInfo.application_id; + + // Just in case. + if (s_TitleInfoMap.find(ApplicationID) == s_TitleInfoMap.end()) + { + s_TitleInfoMap.emplace(std::make_pair(ApplicationID, Data::TitleInfo(ApplicationID))); + } + + PdmPlayStatistics PlayStats = {0}; + Result PDMError = pdmqryQueryPlayStatisticsByApplicationIdAndUserAccountId(ApplicationID, SaveInfo.uid, false, &PlayStats); + if (R_FAILED(PDMError)) + { + // Logged, but not fatal. + Logger::Log("Error getting play stats for %016llX: 0x%X", ApplicationID, PDMError); + } + + FindUser->second.AddToMap(SaveInfo, PlayStats); + } + } + return true; +} diff --git a/Source/Data/TitleInfo.cpp b/Source/Data/TitleInfo.cpp new file mode 100644 index 0000000..22d44ac --- /dev/null +++ b/Source/Data/TitleInfo.cpp @@ -0,0 +1,63 @@ +#include "Data/TitleInfo.hpp" +#include "Colors.hpp" +#include "StringUtil.hpp" +#include + +Data::TitleInfo::TitleInfo(uint64_t ApplicationID) +{ + // Used to calculate icon size. + uint64_t NsAppControlSize = 0; + // Actual control data. + NsApplicationControlData NsControlData = {0}; + // Language entry + NacpLanguageEntry *LanguageEntry = nullptr; + + Result NsError = nsGetApplicationControlData(NsApplicationControlSource_Storage, + ApplicationID, + &NsControlData, + sizeof(NsApplicationControlData), + &NsAppControlSize); + + if (R_FAILED(NsError) || NsAppControlSize < sizeof(NsControlData.nacp)) + { + std::string ApplicationIDHex = StringUtil::GetFormattedString("%04X", ApplicationID & 0xFFFF); + // Blank the nacp just to be sure. + std::memset(&m_NACP, 0x00, sizeof(NacpStruct)); + // Create a place holder icon. + int TextX = 128 - (SDL::Text::GetWidth(32, ApplicationIDHex.c_str()) / 2); + m_Icon = SDL::TextureManager::CreateLoadTexture(ApplicationIDHex, 256, 256, SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET); + SDL::Text::Render(m_Icon->Get(), TextX, 112, 32, SDL::Text::NO_TEXT_WRAP, Colors::White, ApplicationIDHex.c_str()); + } + else if (R_SUCCEEDED(NsError) && R_SUCCEEDED(nacpGetLanguageEntry(&NsControlData.nacp, &LanguageEntry))) + { + // Memcpy the NACP since it has all the good stuff. + std::memcpy(&m_NACP, &NsControlData.nacp, sizeof(NacpStruct)); + // Load the icon. + m_Icon = SDL::TextureManager::CreateLoadTexture(LanguageEntry->name, NsControlData.icon, NsAppControlSize - sizeof(NacpStruct)); + } +} + +const char *Data::TitleInfo::GetTitle(void) +{ + NacpLanguageEntry *Entry = nullptr; + if (R_FAILED(nacpGetLanguageEntry(&m_NACP, &Entry))) + { + return nullptr; + } + return Entry->name; +} + +const char *Data::TitleInfo::GetPublisher(void) +{ + NacpLanguageEntry *Entry = nullptr; + if (R_FAILED(nacpGetLanguageEntry(&m_NACP, &Entry))) + { + return nullptr; + } + return Entry->author; +} + +SDL::SharedTexture Data::TitleInfo::GetIcon(void) const +{ + return m_Icon; +} diff --git a/Source/Data/User.cpp b/Source/Data/User.cpp new file mode 100644 index 0000000..6d4a6eb --- /dev/null +++ b/Source/Data/User.cpp @@ -0,0 +1,117 @@ +#include "Data/User.hpp" +#include "Colors.hpp" +#include "Config.hpp" +#include "Logger.hpp" +#include "SDL.hpp" +#include "StringUtil.hpp" +#include + +namespace +{ + constexpr int ICON_FONT_SIZE = 42; +} + +Data::User::User(AccountUid AccountID) : m_AccountID(AccountID) +{ + AccountProfile Profile; + AccountProfileBase ProfileBase = {0}; + + // Whoever named these needs some help. What the hell? + Result ProfileError = accountGetProfile(&Profile, m_AccountID); + Result ProfileBaseError = accountProfileGet(&Profile, NULL, &ProfileBase); + if (R_FAILED(ProfileError) || R_FAILED(ProfileBaseError)) + { + User::CreateAccount(); + } + else + { + User::LoadAccount(Profile, ProfileBase); + } + accountProfileClose(&Profile); +} + +Data::User::User(AccountUid AccountID, std::string_view Nickname, std::string_view PathSafeNickname) : m_AccountID(AccountID) +{ + // Memcpy nicknames. This is actually really unsafe, but I know what's going to be copied here. + std::memcpy(m_Nickname, Nickname.data(), Nickname.length()); + std::memcpy(m_PathSafeNickname, PathSafeNickname.data(), PathSafeNickname.length()); + + // Create Icon + int TextX = 128 - (SDL::Text::GetWidth(32, m_Nickname) / 2); + m_Icon = SDL::TextureManager::CreateLoadTexture(m_Nickname, 256, 256, SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET); + SDL::Text::Render(m_Icon->Get(), TextX, 112, 32, SDL::Text::NO_TEXT_WRAP, Colors::White, m_Nickname); +} + +void Data::User::AddToMap(const FsSaveDataInfo &SaveInfo, const PdmPlayStatistics &PlayStats) +{ + uint64_t ApplicationID = SaveInfo.application_id == 0 ? SaveInfo.system_save_data_id : SaveInfo.application_id; + std::memcpy(&m_UserDataMap[ApplicationID].first, &SaveInfo, sizeof(FsSaveDataInfo)); + std::memcpy(&m_UserDataMap[ApplicationID].second, &PlayStats, sizeof(PdmPlayStatistics)); +} + +FsSaveDataInfo *Data::User::GetSaveInfoByID(uint64_t ApplicationID) +{ + if (m_UserDataMap.find(ApplicationID) == m_UserDataMap.end()) + { + return nullptr; + } + return &m_UserDataMap.at(ApplicationID).first; +} + +PdmPlayStatistics *Data::User::GetPlayStatsByID(uint64_t ApplicationID) +{ + if (m_UserDataMap.find(ApplicationID) == m_UserDataMap.end()) + { + return nullptr; + } + return &m_UserDataMap.at(ApplicationID).second; +} + +void Data::User::LoadAccount(AccountProfile &Profile, AccountProfileBase &ProfileBase) +{ + // Try to load icon. + uint32_t IconSize = 0; + Result AccountError = accountProfileGetImageSize(&Profile, &IconSize); + if (R_FAILED(AccountError)) + { + Logger::Log("Error getting user icon size: 0x%X.", AccountError); + User::CreateAccount(); + return; + } + + std::unique_ptr IconBuffer(new unsigned char[IconSize]); + AccountError = accountProfileLoadImage(&Profile, IconBuffer.get(), IconSize, &IconSize); + if (R_FAILED(AccountError)) + { + Logger::Log("Error loading user icon: 0x%08X.", AccountError); + User::CreateAccount(); + return; + } + + // We should be good at this point. + m_Icon = SDL::TextureManager::CreateLoadTexture(ProfileBase.nickname, IconBuffer.get(), IconSize); + + // Memcpy the nickname. + std::memcpy(m_Nickname, &ProfileBase.nickname, 0x20); + + if (!StringUtil::SanitizeStringForPath(m_Nickname, m_PathSafeNickname, 0x20)) + { + std::string AccountIDString = StringUtil::GetFormattedString("Account_%08X", m_AccountID.uid[0] & 0xFFFFFFFF); + std::memcpy(m_PathSafeNickname, AccountIDString.c_str(), AccountIDString.length()); + } +} + +void Data::User::CreateAccount(void) +{ + // This is needed a lot here. + std::string AccountIDString = StringUtil::GetFormattedString("Account_%08X", m_AccountID.uid[0] & 0xFFFFFFFF); + + // Create icon + int TextX = 128 - (SDL::Text::GetWidth(32, AccountIDString.c_str()) / 2); + m_Icon = SDL::TextureManager::CreateLoadTexture(AccountIDString, 256, 256, SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET); + SDL::Text::Render(m_Icon->Get(), TextX, 112, 32, SDL::Text::NO_TEXT_WRAP, Colors::White, AccountIDString.c_str()); + + // Memcpy the id string for both nicknames + std::memcpy(m_Nickname, AccountIDString.c_str(), AccountIDString.length()); + std::memcpy(m_PathSafeNickname, AccountIDString.c_str(), AccountIDString.length()); +} diff --git a/Source/JKSV.cpp b/Source/JKSV.cpp index c3e03bd..51f9f41 100644 --- a/Source/JKSV.cpp +++ b/Source/JKSV.cpp @@ -1,5 +1,8 @@ #include "JKSV.hpp" +#include "AppStates/MainMenuState.hpp" #include "Colors.hpp" +#include "Config.hpp" +#include "Data/Data.hpp" #include "FsLib.hpp" #include "Input.hpp" #include "Logger.hpp" @@ -7,8 +10,6 @@ #include "Strings.hpp" #include -#include "UI/RenderFunctions.hpp" - #define ABORT_ON_FAILURE(x) \ if (!x) \ { \ @@ -36,11 +37,15 @@ static bool InitializeService(Result (*Function)(Args...), const char *ServiceNa JKSV::JKSV(void) { + // FsLib ABORT_ON_FAILURE(FsLib::Initialize()); + // This doesn't really on stdio or anything. + Logger::Initialize(); // Need to init RomFS here for now until I update FsLib to take care of this. ABORT_ON_FAILURE(InitializeService(romfsInit, "RomFS")); // Let FsLib take care of calls to SDMC instead of fs_dev ABORT_ON_FAILURE(FsLib::Dev::InitializeSDMC()); + // SDL ABORT_ON_FAILURE(SDL::Initialize("JKSV", 1280, 720)); ABORT_ON_FAILURE(SDL::Text::Initialize()); @@ -56,9 +61,20 @@ JKSV::JKSV(void) ABORT_ON_FAILURE(InitializeService(setsysInitialize, "SetSys")); ABORT_ON_FAILURE(InitializeService(socketInitializeDefault, "Socket")); - // This needs Set. JKSV also has no internal strings anymore. This is FATAL now. + // Input doesn't have anything to return. + Input::Initialize(); + + // Neither does config. + Config::Initialize(); + + // JKSV also has no internal strings anymore. This is FATAL now. ABORT_ON_FAILURE(Strings::Initialize()); + if (!Data::Initialize()) + { + return; + } + // Install/setup our color changing characters. SDL::Text::AddColorCharacter(L'#', Colors::Blue); SDL::Text::AddColorCharacter(L'*', Colors::Red); @@ -71,7 +87,8 @@ JKSV::JKSV(void) // This can't be in an initializer list because it needs SDL initialized. m_HeaderIcon = SDL::TextureManager::CreateLoadTexture("HeaderIcon", "romfs:/Textures/HeaderIcon.png"); - Input::Initialize(); + // Push initial main menu state. + JKSV::PushState(std::make_shared()); m_IsRunning = true; } @@ -104,6 +121,16 @@ void JKSV::Update(void) { m_IsRunning = false; } + + if (!m_StateVector.empty()) + { + while (!m_StateVector.back()->IsActive()) + { + m_StateVector.pop_back(); + m_StateVector.back()->GiveFocus(); + } + m_StateVector.back()->Update(); + } } void JKSV::Render(void) @@ -121,21 +148,33 @@ void JKSV::Render(void) { SDL::Text::Render(NULL, 8, - 682, - 12, + 680, + 14, SDL::Text::NO_TEXT_WRAP, Colors::White, Strings::GetByName(Strings::Names::TranslationInfo, 0), Strings::GetByName(Strings::Names::TranslationInfo, 1)); } - SDL::Text::Render(NULL, 8, 700, 12, SDL::Text::NO_TEXT_WRAP, Colors::White, "v %02d.%02d.%04d", BUILD_MON, BUILD_DAY, BUILD_YEAR); - UI::RenderDialogBox(NULL, 320, 240, 320, 240); + if (!m_StateVector.empty()) + { + for (auto &CurrentState : m_StateVector) + { + CurrentState->Render(); + } + } + + SDL::Text::Render(NULL, 8, 700, 14, SDL::Text::NO_TEXT_WRAP, Colors::White, "v. %02d.%02d.%04d", BUILD_MON, BUILD_DAY, BUILD_YEAR); SDL::FrameEnd(); } void JKSV::PushState(std::shared_ptr NewState) { + if (!m_StateVector.empty()) + { + m_StateVector.back()->TakeFocus(); + } + NewState->GiveFocus(); m_StateVector.push_back(NewState); } diff --git a/Source/StringUtil.cpp b/Source/StringUtil.cpp index 81935be..70a5cfe 100644 --- a/Source/StringUtil.cpp +++ b/Source/StringUtil.cpp @@ -1,10 +1,18 @@ #include "StringUtil.hpp" +#include +#include #include +#include +#include namespace { + // Size limit for formatted strings. constexpr size_t VA_BUFFER_SIZE = 0x1000; -} + // These characters get replaced by spaces when path is sanitized. + constexpr std::array FORBIDDEN_PATH_CHARACTERS = + {L',', L'/', L'\\', L'<', L'>', L':', L'"', L'|', L'?', L'*', L'™', L'©', L'®'}; +} // namespace std::string StringUtil::GetFormattedString(const char *Format, ...) { @@ -17,3 +25,57 @@ std::string StringUtil::GetFormattedString(const char *Format, ...) return std::string(VaBuffer); } + +void StringUtil::ReplaceInString(std::string &Target, std::string_view Find, std::string_view Replace) +{ + size_t StringPosition = 0; + while ((StringPosition = Target.find(Find, StringPosition)) != Target.npos) + { + Target.replace(StringPosition, Find.length(), Replace); + } +} + +bool StringUtil::SanitizeStringForPath(const char *StringIn, char *StringOut, size_t StringOutSize) +{ + uint32_t Codepoint = 0; + size_t StringLength = std::strlen(StringIn); + for (size_t i = 0, StringOutOffset = 0; i < StringLength;) + { + ssize_t UnitCount = decode_utf8(&Codepoint, reinterpret_cast(&StringIn[i])); + if (UnitCount <= 0 || i + UnitCount >= StringOutSize) + { + break; + } + + if (Codepoint < 0x20 || Codepoint > 0x7E) + { + // Don't even bother. It's not possible. + return false; + } + + // Replace forbidden with spaces. + if (std::find(FORBIDDEN_PATH_CHARACTERS.begin(), FORBIDDEN_PATH_CHARACTERS.end(), Codepoint) != FORBIDDEN_PATH_CHARACTERS.end()) + { + StringOut[StringOutOffset++] = 0x20; + } + else if (Codepoint == L'é') + { + StringOut[StringOutOffset++] = 'e'; + } + else + { + // Just memcpy it over. This is a safety thing to be honest. Since it's only Ascii allowed, unitcount should only be 1. + std::memcpy(&StringOut[StringOutOffset], &StringIn[i], static_cast(UnitCount)); + StringOutOffset += UnitCount; + } + i += UnitCount; + } + + // Loop backwards and trim off spaces and periods. + size_t StringOutLength = std::strlen(StringOut); + while (StringOut[StringOutLength - 1] == ' ' || StringOut[StringOutLength - 1] == '.') + { + StringOut[--StringOutLength] = 0x00; + } + return true; +} diff --git a/Source/Strings.cpp b/Source/Strings.cpp index 6dac94b..1d46e00 100644 --- a/Source/Strings.cpp +++ b/Source/Strings.cpp @@ -1,6 +1,7 @@ #include "Strings.hpp" #include "FsLib.hpp" #include "JSON.hpp" +#include "StringUtil.hpp" #include #include #include @@ -30,6 +31,7 @@ namespace {SetLanguage_PTBR, "PTBR.json"}}; } // namespace +// This returns the language file to use depending on the system's language. static FsLib::Path GetStringFilePath(void) { FsLib::Path ReturnPath = "romfs:/Text"; @@ -50,6 +52,27 @@ static FsLib::Path GetStringFilePath(void) return ReturnPath / s_FileMap.at(Language); } +static void ReplaceButtonsInString(std::string &Target) +{ + StringUtil::ReplaceInString(Target, "[A]", "\ue0e0"); + StringUtil::ReplaceInString(Target, "[B]", "\ue0e1"); + StringUtil::ReplaceInString(Target, "[X]", "\ue0e2"); + StringUtil::ReplaceInString(Target, "[Y]", "\ue0e3"); + StringUtil::ReplaceInString(Target, "[L]", "\ue0e4"); + StringUtil::ReplaceInString(Target, "[R]", "\ue0e5"); + StringUtil::ReplaceInString(Target, "[ZL]", "\ue0e6"); + StringUtil::ReplaceInString(Target, "[ZR]", "\ue0e7"); + StringUtil::ReplaceInString(Target, "[SL]", "\ue0e8"); + StringUtil::ReplaceInString(Target, "[SR]", "\ue0e9"); + StringUtil::ReplaceInString(Target, "[DPAD]", "\ue0ea"); + StringUtil::ReplaceInString(Target, "[DUP]", "\ue0eb"); + StringUtil::ReplaceInString(Target, "[DDOWN]", "\ue0ec"); + StringUtil::ReplaceInString(Target, "[DLEFT]", "\ue0ed"); + StringUtil::ReplaceInString(Target, "[DRIGHT]", "\ue0ee"); + StringUtil::ReplaceInString(Target, "[+]", "\ue0ef"); + StringUtil::ReplaceInString(Target, "[-]", "\ue0f0"); +} + bool Strings::Initialize() { FsLib::Path StringsPath = GetStringFilePath(); @@ -77,6 +100,13 @@ bool Strings::Initialize() } json_object_iter_next(&StringIterator); } + + // Loop through entire map and replace the buttons. + for (auto &[Key, String] : s_StringMap) + { + ReplaceButtonsInString(String); + } + return true; } diff --git a/Source/UI/ColorMod.cpp b/Source/UI/ColorMod.cpp new file mode 100644 index 0000000..c055910 --- /dev/null +++ b/Source/UI/ColorMod.cpp @@ -0,0 +1,18 @@ +#include "UI/ColorMod.hpp" + +void UI::ColorMod::Update(void) +{ + if (m_Direction && (m_ColorMod += 6) >= 0x72) + { + m_Direction = false; + } + else if (!m_Direction && (m_ColorMod -= 3) <= 0x00) + { + m_Direction = true; + } +} + +UI::ColorMod::operator uint8_t(void) const +{ + return m_ColorMod; +} diff --git a/Source/UI/Menu.cpp b/Source/UI/Menu.cpp new file mode 100644 index 0000000..c166d7f --- /dev/null +++ b/Source/UI/Menu.cpp @@ -0,0 +1,124 @@ +#include "UI/Menu.hpp" +#include "Colors.hpp" +#include "Config.hpp" +#include "Input.hpp" +#include "UI/RenderFunctions.hpp" + +UI::Menu::Menu(int X, int Y, int Width, int FontSize, int ScrollLength) + : m_X(X), m_Y(Y), m_OriginalY(Y), m_FontSize(FontSize), m_ScrollLength(ScrollLength), m_MenuRenderLength(ScrollLength * 2), m_Width(Width), + m_Height(FontSize + 32) +{ + static int MenuID = 0; + // Create target for menu option + std::string MenuTargetName = "Menu_" + std::to_string(MenuID++); + m_OptionTarget = + SDL::TextureManager::CreateLoadTexture(MenuTargetName, m_Width, m_Height, SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET); +} + +void UI::Menu::Update(void) +{ + // Bail if there's nothing to update. + if (m_Options.empty()) + { + return; + } + + if (Input::ButtonPressed(HidNpadButton_AnyUp) && --m_Selected < 0) + { + m_Selected = m_OptionsLength; + } + else if (Input::ButtonPressed(HidNpadButton_AnyDown) && ++m_Selected > m_OptionsLength) + { + m_Selected = 0; + } + else if (Input::ButtonPressed(HidNpadButton_Left) && (m_Selected -= m_ScrollLength) < 0) + { + m_Selected = 0; + } + else if (Input::ButtonPressed(HidNpadButton_AnyRight) && (m_Selected += m_ScrollLength) > m_OptionsLength) + { + m_Selected = m_OptionsLength; + } + + // Calculate scrolling + if (m_Selected < m_ScrollLength) + { + m_TargetY = m_OriginalY; + } + else if (m_Selected >= m_ScrollLength && m_OptionsLength > m_MenuRenderLength) + { + m_TargetY = m_OriginalY + -(m_Height * (m_OptionsLength * m_MenuRenderLength)); + } + else if (m_Selected > m_MenuRenderLength && m_Selected < (m_OptionsLength - m_MenuRenderLength)) + { + m_TargetY = -(m_Height * (m_Selected - m_MenuRenderLength)); + } + + if (m_Y != m_TargetY) + { + m_Y += std::ceil((m_TargetY - m_Y) / Config::GetAnimationScaling()); + } +} + +void UI::Menu::Render(SDL_Texture *Target) +{ + if (m_Options.empty()) + { + return; + } + + m_ColorMod.Update(); + + // I hate doing this. + int TargetHeight = 0; + SDL_QueryTexture(Target, NULL, NULL, NULL, &TargetHeight); + for (int i = 0, TempY = m_Y; i < m_OptionsLength; i++, TempY += m_Height) + { + if (TempY < 0 || TempY > TargetHeight) + { + continue; + } + // Clear target texture. + m_OptionTarget->Clear(Colors::Transparent); + + if (i == m_Selected) + { + // Render the bounding box + UI::RenderBoundingBox(Target, m_X - 4, m_Y - 4, m_Width + 8, m_Height + 8, m_ColorMod); + // Render the little rectangle. + SDL::RenderRectFill(m_OptionTarget->Get(), 8, 2, 4, m_Height - 4, {0x00FFC5FF}); + } + // Render text to target. + SDL::Text::Render(m_OptionTarget->Get(), + 14, + (m_Height / 2) - (m_FontSize / 2), + m_FontSize, + SDL::Text::NO_TEXT_WRAP, + i == m_Selected ? Colors::Blue : Colors::White, + m_Options.at(i).c_str()); + // Render target to target + m_OptionTarget->Render(Target, m_X, TempY); + } +} + +void UI::Menu::AddOption(std::string_view NewOption) +{ + m_Options.push_back(NewOption.data()); + ++m_OptionsLength; +} + +int UI::Menu::GetSelected(void) const +{ + return m_Selected; +} + +void UI::Menu::SetSelected(int Selected) +{ + m_Selected = Selected; +} + +void UI::Menu::Reset(void) +{ + m_OptionsLength = -1; + m_Options.clear(); +} diff --git a/Source/UI/RenderFunctions.cpp b/Source/UI/RenderFunctions.cpp index 21ab292..255fc4f 100644 --- a/Source/UI/RenderFunctions.cpp +++ b/Source/UI/RenderFunctions.cpp @@ -25,3 +25,29 @@ void UI::RenderDialogBox(SDL_Texture *Target, int X, int Y, int Width, int Heigh SDL::RenderRectFill(NULL, X + 16, (Y + Height) - 16, Width - 32, 16, Colors::DialogBox); s_DialogCorners->RenderPart(NULL, (X + Width) - 16, (Y + Height) - 16, 16, 16, 16, 16); } + +void UI::RenderBoundingBox(SDL_Texture *Target, int X, int Y, int Width, int Height, uint8_t ColorMod) +{ + if (!s_MenuBoundingCorners) + { + s_MenuBoundingCorners = SDL::TextureManager::CreateLoadTexture("MenuBoundingCorners", "romfs:/Textures/MenuBounding.png"); + } + + // Setup color. + SDL::Color RenderMod = {(0x88 + ColorMod) << 16 | (0xC5 + (ColorMod / 2) << 8) | 0xFF}; + + // This shouldn't fail, but I don't really care if it does. + SDL_SetTextureColorMod(s_MenuBoundingCorners->Get(), RenderMod.RGBA[3], RenderMod.RGBA[2], RenderMod.RGBA[1]); + + // Top + s_MenuBoundingCorners->RenderPart(Target, X, Y, 0, 0, 8, 8); + SDL::RenderRectFill(Target, X + 8, Y, Width - 16, 4, RenderMod); + s_MenuBoundingCorners->RenderPart(Target, (X + Width) - 8, Y, 8, 0, 8, 8); + // Middle + SDL::RenderRectFill(Target, X, Y + 8, 4, Height - 16, RenderMod); + SDL::RenderRectFill(Target, (X + Width) - 4, Y + 8, 4, Height - 16, RenderMod); + // Bottom + s_MenuBoundingCorners->RenderPart(Target, X, (Y + Height) - 8, 0, 8, 8, 8); + SDL::RenderRectFill(Target, X + 8, (Y + Height) - 4, Width - 16, 4, RenderMod); + s_MenuBoundingCorners->RenderPart(Target, (X + Width) - 8, (Y + Height) - 8, 8, 8, 8, 8); +}