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 0000000..d77ab13 Binary files /dev/null and b/RomFS/Textures/MenuBackground.png differ diff --git a/Source/AppStates/MainMenuState.cpp b/Source/AppStates/MainMenuState.cpp new file mode 100644 index 0000000..3b20607 --- /dev/null +++ b/Source/AppStates/MainMenuState.cpp @@ -0,0 +1,28 @@ +#include "AppStates/MainMenuState.hpp" +#include "Colors.hpp" +#include "SDL.hpp" +#include "Strings.hpp" + +MainMenuState::MainMenuState(void) + : m_RenderTarget(SDL::TextureManager::CreateLoadTexture("MainMenuTarget", 200, 555, SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET)), + m_Background(SDL::TextureManager::CreateLoadTexture("MainMenuBackground", "romfs:/Textures/MenuBackground.png")), + m_ControlGuide(Strings::GetByName(Strings::Names::ControlGuides, 0)), m_ControlGuideX(1220 - SDL::Text::GetWidth(20, m_ControlGuide)) {}; + +void MainMenuState::Update(void) +{ +} + +void MainMenuState::Render(void) +{ + // Clear render target by rendering background to it. + m_Background->Render(m_RenderTarget->Get(), 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); +}