diff --git a/Include/AppStates/AppState.hpp b/Include/AppStates/AppState.hpp deleted file mode 100644 index 3d93266..0000000 --- a/Include/AppStates/AppState.hpp +++ /dev/null @@ -1,63 +0,0 @@ -#pragma once -#include "SDL.hpp" -#include - -class AppState -{ - public: - AppState(bool IsClosable = true) : m_IsClosable(IsClosable) {}; - 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 - { - return m_IsActive; - } - - // Deactivates state and allows JKSV to kill it. - void Deactivate(void) - { - m_IsActive = false; - } - - // Tells state it has the current focus of the app. - void GiveFocus(void) - { - m_HasFocus = true; - } - - // Takes focus away from the state. - void TakeFocus(void) - { - m_HasFocus = false; - } - - // Returns whether state is at back of vector and has focus. - bool HasFocus(void) const - { - return m_HasFocus; - } - - // Returns whether or not state is closable with +. - bool IsClosable(void) const - { - return m_IsClosable; - } - - private: - // Whether state is still active or can be purged. - bool m_IsActive = true; - // Whether or not state has focus - bool m_HasFocus = false; - // Whether or not state should allow exiting JKSV - bool m_IsClosable = false; -}; diff --git a/Include/AppStates/BackupMenuState.hpp b/Include/AppStates/BackupMenuState.hpp deleted file mode 100644 index e655e18..0000000 --- a/Include/AppStates/BackupMenuState.hpp +++ /dev/null @@ -1,58 +0,0 @@ -#pragma once -#include "AppStates/AppState.hpp" -#include "Data/Data.hpp" -#include "FsLib.hpp" -#include "SDL.hpp" -#include "System/Timer.hpp" -#include "UI/Menu.hpp" -#include "UI/SlideOutPanel.hpp" -#include - -class BackupMenuState : public AppState -{ - public: - BackupMenuState(Data::User *User, Data::TitleInfo *TitleInfo, FsSaveDataType SaveType); - ~BackupMenuState() {}; - - void Update(void); - void Render(void); - - // Refreshes/Updates menu and listing. - void RefreshListing(void); - - private: - // Pointer to user. - Data::User *m_User; - // Pointer to title info. - Data::TitleInfo *m_TitleInfo; - // Save data type. - FsSaveDataType m_SaveType; - // Backup folder path - FsLib::Path m_DirectoryPath; - // Directory listing of that folder. - FsLib::Directory m_DirectoryListing; - // Width of the title. - int m_TitleWidth; - // X coordinate of title. - int m_TitleX; - // Whether or not the title is too long for the panel and should scroll. - bool m_ScrollTitle = false; - // Whether or not the timer was triggered and we're scrolling. - bool m_ScrollTriggered = false; - // Timer for scrolling title if needed. - System::Timer m_TitleTimer; - // This holds whether or not the static members every instance shares are initialized. - static inline bool m_Initialized = false; - // Menu - static inline std::unique_ptr m_BackupMenu = nullptr; - // Slide panel. - static inline std::unique_ptr m_SlidePanel = nullptr; - // Render target for menu. - static inline SDL::SharedTexture m_MenuTarget = nullptr; - // Width of panel. - static inline int m_PanelWidth = 0; - // X coordinate of text above menu. - static inline int m_CurrentBackupsCoordinate = 0; - // Function to render the title to the menu. - void RenderTitle(void); -}; diff --git a/Include/AppStates/ConfirmState.hpp b/Include/AppStates/ConfirmState.hpp deleted file mode 100644 index b9faa01..0000000 --- a/Include/AppStates/ConfirmState.hpp +++ /dev/null @@ -1,108 +0,0 @@ -#pragma once -#include "AppStates/AppState.hpp" -#include "AppStates/ProgressState.hpp" -#include "AppStates/TaskState.hpp" -#include "Colors.hpp" -#include "Input.hpp" -#include "JKSV.hpp" -#include "SDL.hpp" -#include "Strings.hpp" -#include "System/Task.hpp" -#include "UI/RenderFunctions.hpp" -#include -#include -#include - -template -class ConfirmState : public AppState -{ - public: - // All functions using confirmation must follow this signature. - using TaskFunction = void (*)(TaskType *, std::shared_ptr); - // Constructor - ConfirmState(std::string_view QueryString, bool HoldRequired, TaskFunction Function, std::shared_ptr DataStruct) - : AppState(false), m_QueryString(QueryString.data()), m_YesString(Strings::GetByName(Strings::Names::YesNo, 0)), - m_Hold(HoldRequired), m_Function(Function), m_DataStruct(DataStruct) - { - appletBeginBlockingHomeButton(0); - } - - ~ConfirmState() - { - appletEndBlockingHomeButton(); - } - - void Update(void) - { - if (Input::ButtonPressed(HidNpadButton_A) && !m_Hold) - { - AppState::Deactivate(); - JKSV::PushState(std::make_shared(m_Function, m_DataStruct)); - } - else if (Input::ButtonPressed(HidNpadButton_A) && m_Hold) - { - // Get the starting tick count and change the Yes string to the first holding string. - m_StartingTickCount = SDL_GetTicks64(); - m_YesString = Strings::GetByName(Strings::Names::HoldingStrings, 0); - } - else if (Input::ButtonHeld(HidNpadButton_A) && m_Hold) - { - uint64_t TickCount = SDL_GetTicks64() - m_StartingTickCount; - - // If the TickCount is >= 3 seconds, confirmed. Else, just change the string so we can see we're not holding for nothing? - if (TickCount >= 3000) - { - AppState::Deactivate(); - JKSV::PushState(std::make_shared(m_Function, m_DataStruct)); - } - else if (TickCount >= 2000) - { - m_YesString = Strings::GetByName(Strings::Names::HoldingStrings, 2); - } - else if (TickCount >= 1000) - { - m_YesString = Strings::GetByName(Strings::Names::HoldingStrings, 1); - } - } - else if (Input::ButtonReleased(HidNpadButton_A)) - { - m_YesString = Strings::GetByName(Strings::Names::YesNo, 0); - } - else if (Input::ButtonPressed(HidNpadButton_B)) - { - // Just deactivate and don't do anything. - AppState::Deactivate(); - } - } - - void Render(void) - { - // Dim background - SDL::RenderRectFill(NULL, 0, 0, 1280, 720, Colors::BackgroundDim); - // Render dialog - UI::RenderDialogBox(NULL, 280, 262, 720, 256); - // Text - SDL::Text::Render(NULL, 312, 288, 18, 656, Colors::White, m_QueryString.c_str()); - // Fake buttons. Maybe real later. - SDL::RenderLine(NULL, 280, 454, 999, 454, Colors::White); - SDL::RenderLine(NULL, 640, 454, 640, 517, Colors::White); - // To do: Position this better. Currently brought over from old code. - int YesX = 458 - SDL::Text::GetWidth(22, m_YesString.c_str()); - SDL::Text::Render(NULL, YesX, 478, 22, SDL::Text::NO_TEXT_WRAP, Colors::White, m_YesString.c_str()); - SDL::Text::Render(NULL, 782, 478, 22, SDL::Text::NO_TEXT_WRAP, Colors::White, Strings::GetByName(Strings::Names::YesNo, 1)); - } - - private: - // Query string - std::string m_QueryString; - // Yes string. - std::string m_YesString; - // Whether or not holding is required to confirm. - bool m_Hold; - // For tick counting/holding - uint64_t m_StartingTickCount = 0; - // Function - TaskFunction m_Function; - // Shared ptr to data to send to confirmation function. - std::shared_ptr m_DataStruct; -}; diff --git a/Include/AppStates/ExtrasMenuState.hpp b/Include/AppStates/ExtrasMenuState.hpp deleted file mode 100644 index dc0b034..0000000 --- a/Include/AppStates/ExtrasMenuState.hpp +++ /dev/null @@ -1,20 +0,0 @@ -#pragma once -#include "AppStates/AppState.hpp" -#include "SDL.hpp" -#include "UI/Menu.hpp" - -class ExtrasMenuState : public AppState -{ - public: - ExtrasMenuState(void); - ~ExtrasMenuState() {}; - - void Update(void); - void Render(void); - - private: - // Actual menu - UI::Menu m_ExtrasMenu; - // Render target - SDL::SharedTexture m_RenderTarget; -}; diff --git a/Include/AppStates/MainMenuState.hpp b/Include/AppStates/MainMenuState.hpp deleted file mode 100644 index da0bd2e..0000000 --- a/Include/AppStates/MainMenuState.hpp +++ /dev/null @@ -1,39 +0,0 @@ -#pragma once -#include "AppStates/AppState.hpp" -#include "Data/Data.hpp" -#include "SDL.hpp" -#include "UI/IconMenu.hpp" - -class MainMenuState : public AppState -{ - public: - MainMenuState(void); - ~MainMenuState() {}; - - void Update(void); - void Render(void); - - // This allows other parts of the program to signal to this one to refresh the view states on the next Update() call. - static void RefreshViewStates(void); - - private: - // The render target. - SDL::SharedTexture m_RenderTarget = nullptr; - // Background of the menu. - SDL::SharedTexture m_Background = nullptr; - // Icons for the last two. - SDL::SharedTexture m_SettingsIcon = nullptr; - SDL::SharedTexture m_ExtrasIcon = nullptr; - // Icon menu for users - UI::IconMenu m_MainMenu; - // Vector of pointers to users. - static inline std::vector m_Users; - // Vector of view states for each user, settings, and extras - static inline std::vector> m_States; - // 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; - // Variable that holds refresh signal. - static inline bool m_RefreshNeeded = false; -}; diff --git a/Include/AppStates/ProgressState.hpp b/Include/AppStates/ProgressState.hpp deleted file mode 100644 index a9c1735..0000000 --- a/Include/AppStates/ProgressState.hpp +++ /dev/null @@ -1,36 +0,0 @@ -#pragma once -#include "AppStates/AppState.hpp" -#include "System/ProgressTask.hpp" -#include -#include - -class ProgressState : public AppState -{ - public: - template - ProgressState(void (*Function)(System::ProgressTask *, Args...), Args... Arguments) - : AppState(false), m_Task(Function, std::forward(Arguments)...) - { - appletBeginBlockingHomeButton(0); - } - - ~ProgressState() - { - appletEndBlockingHomeButton(); - } - - void Update(void); - void Render(void); - - private: - // Underlying progress tracking task. - System::ProgressTask m_Task; - // Progress as whole number instead of decimal. - size_t m_Progress = 0; - // Width of the green bar. The other is hard coded for 720px. - size_t m_ProgressBarWidth = 0; - // X coordinate of the percentage. - int m_PerentageX = 0; - // String for percentage. - std::string m_PercentageString; -}; diff --git a/Include/AppStates/SaveCreateState.hpp b/Include/AppStates/SaveCreateState.hpp deleted file mode 100644 index 80a8d53..0000000 --- a/Include/AppStates/SaveCreateState.hpp +++ /dev/null @@ -1,29 +0,0 @@ -#pragma once -#include "AppStates/AppState.hpp" -#include "AppStates/TitleSelectCommon.hpp" -#include "Data/Data.hpp" -#include "UI/Menu.hpp" -#include "UI/SlideOutPanel.hpp" -#include - -class SaveCreateState : public AppState -{ - public: - // Takes pointer to target user & their title selection for rendering and refreshing. - SaveCreateState(Data::User *TargetUser, TitleSelectCommon *TitleSelect); - ~SaveCreateState() {}; - - void Update(void); - void Render(void); - - private: - // Pointer to user and title select - Data::User *m_User; - TitleSelectCommon *m_TitleSelect; - // Menu - UI::Menu m_SaveMenu; - // Vector of pointers to save info we're using - std::vector m_TitleInfoVector; - // All instances shared this so they're static. - static inline std::unique_ptr m_SlidePanel = nullptr; -}; diff --git a/Include/AppStates/SettingsState.hpp b/Include/AppStates/SettingsState.hpp deleted file mode 100644 index 480952e..0000000 --- a/Include/AppStates/SettingsState.hpp +++ /dev/null @@ -1,22 +0,0 @@ -#pragma once -#include "AppStates/AppState.hpp" -#include "SDL.hpp" -#include "UI/Menu.hpp" - -class SettingsState : public AppState -{ - public: - SettingsState(void); - ~SettingsState() {}; - - void Update(void); - void Render(void); - - private: - // Menu containing options. - UI::Menu m_SettingsMenu; - // Render target. - SDL::SharedTexture m_RenderTarget = nullptr; - // Control guide X coordinate. - int m_ControlGuideX = 0; -}; diff --git a/Include/AppStates/TaskState.hpp b/Include/AppStates/TaskState.hpp deleted file mode 100644 index c0fcb8a..0000000 --- a/Include/AppStates/TaskState.hpp +++ /dev/null @@ -1,27 +0,0 @@ -#pragma once -#include "AppStates/AppState.hpp" -#include "System/Task.hpp" -#include - -class TaskState : public AppState -{ - public: - template - TaskState(void (*Function)(System::Task *, Args...), Args... Arguments) - : AppState(false), m_Task(Function, std::forward(Arguments)...) - { - appletBeginBlockingHomeButton(0); - } - - ~TaskState() - { - appletEndBlockingHomeButton(); - } - - void Update(void); - void Render(void); - - private: - // Underlying task. - System::Task m_Task; -}; diff --git a/Include/AppStates/TextTitleSelectState.hpp b/Include/AppStates/TextTitleSelectState.hpp deleted file mode 100644 index 57d88b9..0000000 --- a/Include/AppStates/TextTitleSelectState.hpp +++ /dev/null @@ -1,25 +0,0 @@ -#pragma once -#include "AppStates/TitleSelectCommon.hpp" -#include "Data/Data.hpp" -#include "SDL.hpp" -#include "UI/Menu.hpp" - -class TextTitleSelectState : public TitleSelectCommon -{ - public: - TextTitleSelectState(Data::User *User); - ~TextTitleSelectState() {}; - - void Update(void); - void Render(void); - - void Refresh(void); - - private: - // Pointer to user to save. - Data::User *m_User; - // Title select menu - UI::Menu m_TitleSelectMenu; - // Render target - SDL::SharedTexture m_RenderTarget; -}; diff --git a/Include/AppStates/TitleSelectCommon.hpp b/Include/AppStates/TitleSelectCommon.hpp deleted file mode 100644 index 507776f..0000000 --- a/Include/AppStates/TitleSelectCommon.hpp +++ /dev/null @@ -1,20 +0,0 @@ -#pragma once -#include "AppStates/AppState.hpp" - -class TitleSelectCommon : public AppState -{ - public: - TitleSelectCommon(void); - virtual ~TitleSelectCommon() {}; - - virtual void Update(void) = 0; - virtual void Render(void) = 0; - - virtual void Refresh(void) = 0; - - void RenderControlGuide(void); - - protected: - // X coordinate for control guide. Shared between all instances. Only should be calculated once. - static inline int m_TitleControlsX = 0; -}; diff --git a/Include/AppStates/TitleSelectState.hpp b/Include/AppStates/TitleSelectState.hpp deleted file mode 100644 index 622227a..0000000 --- a/Include/AppStates/TitleSelectState.hpp +++ /dev/null @@ -1,25 +0,0 @@ -#pragma once -#include "AppStates/TitleSelectCommon.hpp" -#include "Data/Data.hpp" -#include "SDL.hpp" -#include "UI/TitleView.hpp" - -class TitleSelectState : public TitleSelectCommon -{ - public: - TitleSelectState(Data::User *User); - ~TitleSelectState() {}; - - void Update(void); - void Render(void); - - void Refresh(void); - - private: - // Save pointer to user - Data::User *m_User = nullptr; - // Render target. - SDL::SharedTexture m_RenderTarget = nullptr; - // Title select view - UI::TitleView m_TitleView; -}; diff --git a/Include/AppStates/UserOptionState.hpp b/Include/AppStates/UserOptionState.hpp deleted file mode 100644 index 66b6a65..0000000 --- a/Include/AppStates/UserOptionState.hpp +++ /dev/null @@ -1,28 +0,0 @@ -#pragma once -#include "AppStates/AppState.hpp" -#include "Data/User.hpp" -#include "TitleSelectCommon.hpp" -#include "UI/Menu.hpp" -#include "UI/SlideOutPanel.hpp" -#include - -class UserOptionState : public AppState -{ - public: - // Takes a pointer to the target user and the view so it can render and refresh it. - UserOptionState(Data::User *User, TitleSelectCommon *TitleSelect); - ~UserOptionState() {}; - - void Update(void); - void Render(void); - - private: - // Target user - Data::User *m_User; - // Title view to render and refresh - TitleSelectCommon *m_TitleSelect; - // Menu - UI::Menu m_UserOptionMenu; - // Static panel shared by all instances. - static inline std::unique_ptr m_MenuPanel = nullptr; -}; diff --git a/Include/Colors.hpp b/Include/Colors.hpp deleted file mode 100644 index e813240..0000000 --- a/Include/Colors.hpp +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once -#include "SDL.hpp" - -namespace Colors -{ - static constexpr SDL::Color White = {0xFFFFFFFF}; - static constexpr SDL::Color Black = {0x000000FF}; - static constexpr SDL::Color Red = {0xFF0000FF}; - static constexpr SDL::Color Green = {0x00FF00FF}; - static constexpr SDL::Color Blue = {0x0099EEFF}; - static constexpr SDL::Color Yellow = {0xF8FC00FF}; - static constexpr SDL::Color Pink = {0xFF4444FF}; - static constexpr SDL::Color BlueGreen = {0x00FFC5FF}; - static constexpr SDL::Color ClearColor = {0x2D2D2DFF}; - static constexpr SDL::Color DialogBox = {0x505050FF}; - static constexpr SDL::Color BackgroundDim = {0x00000088}; - static constexpr SDL::Color Transparent = {0x00000000}; - static constexpr SDL::Color SlidePanelClear = {0x000000CC}; -} // namespace Colors diff --git a/Include/Config.hpp b/Include/Config.hpp deleted file mode 100644 index 1585e8d..0000000 --- a/Include/Config.hpp +++ /dev/null @@ -1,56 +0,0 @@ -#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); - // Retrieves value by index. - uint8_t GetByIndex(int Index); - // 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 AutoNameBackups = "AutoNameBackups"; - static constexpr std::string_view AutoUpload = "AutoUploadToRemote"; - 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 ZipCompressionLevel = "ZipCompressionLevel"; - static constexpr std::string_view TitleSortType = "TitleSortType"; - static constexpr std::string_view JKSMTextMode = "JKSMTextMode"; - static constexpr std::string_view ForceEnglish = "ForceEnglish"; - static constexpr std::string_view EnableTrashBin = "EnableTrash"; - static constexpr std::string_view UIAnimationScaling = "UIAnimationScaling"; - static constexpr std::string_view Favorites = "Favorites"; - static constexpr std::string_view BlackList = "BlackList"; - } // namespace Keys -} // namespace Config diff --git a/Include/Data/AccountUID.hpp b/Include/Data/AccountUID.hpp deleted file mode 100644 index 4fb851a..0000000 --- a/Include/Data/AccountUID.hpp +++ /dev/null @@ -1,12 +0,0 @@ -#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 bool operator==(AccountUid AccountID, u128 u128ID) -{ - return AccountID.uid[0] == (u128ID >> 64 & 0xFFFFFFFFFFFFFFFF) && AccountID.uid[1] == (u128ID & 0xFFFFFFFFFFFFFFFF); -} diff --git a/Include/Data/Data.hpp b/Include/Data/Data.hpp deleted file mode 100644 index ee7eb7d..0000000 --- a/Include/Data/Data.hpp +++ /dev/null @@ -1,17 +0,0 @@ -#pragma once -#include "Data/AccountUID.hpp" -#include "Data/TitleInfo.hpp" -#include "Data/User.hpp" -#include - -namespace Data -{ - // Loads data from system. - bool Initialize(void); - // Gets a vector of pointers to users. - void GetUsers(std::vector &VectorOut); - // Gets the TitleInfo mapped to ApplicationID. - Data::TitleInfo *GetTitleInfoByID(uint64_t ApplicationID); - // Gets a vector of pointers to all TitleInfo in map according to save type. - void GetTitleInfoByType(FsSaveDataType SaveType, std::vector &TitleInfoOut); -} // namespace Data diff --git a/Include/Data/TitleInfo.hpp b/Include/Data/TitleInfo.hpp deleted file mode 100644 index 767fdcf..0000000 --- a/Include/Data/TitleInfo.hpp +++ /dev/null @@ -1,44 +0,0 @@ -#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 path safe title - const char *GetPathSafeTitle(void); - // Returns publisher - const char *GetPublisher(void); - // Returns the save data owner/application id - uint64_t GetSaveDataOwnerID(void) const; - // Returns save data size for save type. 0 on default. - uint64_t GetSaveDataSize(FsSaveDataType SaveType) const; - // Returns save data size max for save type. 0 on default. - uint64_t GetSaveDataSizeMax(FsSaveDataType SaveType) const; - // Returns journal size for given save type. 0 on default. - uint64_t GetJournalSize(FsSaveDataType SaveType) const; - // Returns the max journal size for save data type. 0 on default. - uint64_t GetJournalSizeMax(FsSaveDataType SaveType) const; - // Returns whether or not title has save data for type. Tests if NACP size is 0, basically. - bool HasSaveDataType(FsSaveDataType SaveType); - - // Returns icon - SDL::SharedTexture GetIcon(void) const; - - private: - // This is where all the important stuff is. - NacpStruct m_NACP; - // This is the path safe version of the title. - char m_PathSafeTitle[0x200] = {0}; - // This is the icon. - SDL::SharedTexture m_Icon = nullptr; - }; -} // namespace Data diff --git a/Include/Data/User.hpp b/Include/Data/User.hpp deleted file mode 100644 index 01be407..0000000 --- a/Include/Data/User.hpp +++ /dev/null @@ -1,64 +0,0 @@ -#pragma once -#include "SDL.hpp" -#include -#include -#include -#include - -namespace Data -{ - // This should make things fun to read :^) - using UserDataEntry = std::pair>; - - class User - { - public: - // This is for normal use accounts. - User(AccountUid AccountID); - // This is for system type accounts. - User(AccountUid AccountID, std::string_view PathSafeNickname, std::string_view IconPath); - - // Adds Data to UserDataMap - void AddData(const FsSaveDataInfo &SaveInfo, const PdmPlayStatistics &PlayStats); - // Runs the title sorting algorithm. - void SortData(void); - // Returns Account ID - AccountUid GetAccountID(void) const; - // Returns nickname - const char *GetNickname(void) const; - // Returns "path safe" nickname - const char *GetPathSafeNickname(void) const; - // Returns total entries. - size_t GetTotalDataEntries(void) const; - // Returns application ID at index - uint64_t GetApplicationIDAt(int Index) const; - // Returns FsSaveDataInfo by index. - FsSaveDataInfo *GetSaveInfoAt(int Index); - // Returns PlayStats by ^ - PdmPlayStatistics *GetPlayStatsAt(int Index); - // Returns FsSaveDataInfo by ApplicationID or SystemSaveID. - FsSaveDataInfo *GetSaveInfoByID(uint64_t ApplicationID); - // Returns PlayStats according to ^ - PdmPlayStatistics *GetPlayStatsByID(uint64_t ApplicationID); - // Returns raw pointer to icon. - SDL_Texture *GetIcon(void); - // Returns shared pointer to icon and increases reference count. - SDL::SharedTexture GetSharedIcon(void); - - private: - // User's ID. - AccountUid m_AccountID; - // Nickname - char m_Nickname[0x20] = {0}; - // Path safe nickname. - char m_PathSafeNickname[0x20] = {0}; - // User's icon - SDL::SharedTexture m_Icon = nullptr; - // Map of FsSaveInfo and play stats - std::vector m_UserData; - // 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/FS/FileIO.hpp b/Include/FS/FileIO.hpp deleted file mode 100644 index 1685daf..0000000 --- a/Include/FS/FileIO.hpp +++ /dev/null @@ -1,20 +0,0 @@ -#pragma once -#include "FsLib.hpp" -#include "System/ProgressTask.hpp" -#include - -namespace FS -{ - // Copies the file. Task is optional, but if passed allows updating the progress of the current operation. - void CopyFile(const FsLib::Path &Source, - const FsLib::Path &Destination, - uint64_t JournalSize = 0, - std::string_view CommitDevice = {}, - System::ProgressTask *Task = nullptr); - // Recursively copies Source to Destination. - void CopyDirectory(const FsLib::Path &Source, - const FsLib::Path &Destination, - uint64_t JournalSize = 0, - std::string_view CommitDevice = {}, - System::ProgressTask *Task = nullptr); -} // namespace FS diff --git a/Include/FS/SaveMount.hpp b/Include/FS/SaveMount.hpp deleted file mode 100644 index ec881d0..0000000 --- a/Include/FS/SaveMount.hpp +++ /dev/null @@ -1,13 +0,0 @@ -#pragma once -#include -#include - -namespace FS -{ - // This is just the default global generic save mount device. - static constexpr std::string_view DEFAULT_SAVE_MOUNT = "save"; - // Same as above, but the path used for root. - static constexpr std::string_view DEFAULT_SAVE_PATH = "save:/"; - // This function mounts save data according the save info passed. FsLib's unmount should be used. - bool MountSaveData(const FsSaveDataInfo &SaveInfo, std::string_view DeviceName); -} // namespace FS diff --git a/Include/FS/ZipIO.hpp b/Include/FS/ZipIO.hpp deleted file mode 100644 index 7272302..0000000 --- a/Include/FS/ZipIO.hpp +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once -// Major to do: Stop using minizip and finish the ZipFile class. -#include "FsLib.hpp" -#include "System/ProgressTask.hpp" -#include -#include -#include - -namespace FS -{ - // Copies directory recursively into Destination. - void CopyDirectoryToZip(const FsLib::Path &Source, zipFile Destination, System::ProgressTask *Task = nullptr); - // Recursively copies Source to destination. This is only used for unzipping saves. - void CopyZipToDirectory(unzFile Source, - const FsLib::Path &Destination, - uint64_t JournalSize, - std::string_view CommitDevice, - System::ProgressTask *Task = nullptr); -} // namespace FS diff --git a/Include/Input.hpp b/Include/Input.hpp deleted file mode 100644 index ea33776..0000000 --- a/Include/Input.hpp +++ /dev/null @@ -1,11 +0,0 @@ -#pragma once -#include - -namespace Input -{ - void Initialize(void); - void Update(void); - bool ButtonPressed(HidNpadButton Button); - bool ButtonHeld(HidNpadButton Button); - bool ButtonReleased(HidNpadButton Button); -} // namespace Input diff --git a/Include/JKSV.hpp b/Include/JKSV.hpp deleted file mode 100644 index d1e8465..0000000 --- a/Include/JKSV.hpp +++ /dev/null @@ -1,32 +0,0 @@ -#pragma once -#include "AppStates/AppState.hpp" -#include "SDL.hpp" -#include -#include - -class JKSV -{ - public: - // Initializes JKSV. - JKSV(void); - // Exits JKSV - ~JKSV(); - // Returns whether or not JKSV is actually running. - bool IsRunning(void) const; - // Updates input and back of state vector. - void Update(void); - // Renders base of app and states of vector. - void Render(void); - // Pushes a new state to the back of state vector. - static void PushState(std::shared_ptr NewState); - - private: - // Whether or not initialization was successful. - bool m_IsRunning = false; - // Whether or not translation author wants to be acknowledged. - bool m_ShowTranslationInfo = false; - // Header icon - SDL::SharedTexture m_HeaderIcon = nullptr; - // Vector of states. - static inline std::vector> m_StateVector; -}; diff --git a/Include/Keyboard.hpp b/Include/Keyboard.hpp deleted file mode 100644 index a1e23b9..0000000 --- a/Include/Keyboard.hpp +++ /dev/null @@ -1,8 +0,0 @@ -#pragma once -#include -#include - -namespace Keyboard -{ - bool GetInput(SwkbdType KeyboardType, std::string_view DefaultText, std::string_view Header, char *StringOut, size_t StringLength); -} // namespace Keyboard diff --git a/Include/Logger.hpp b/Include/Logger.hpp deleted file mode 100644 index dc8bf30..0000000 --- a/Include/Logger.hpp +++ /dev/null @@ -1,7 +0,0 @@ -#pragma once - -namespace Logger -{ - void Initialize(void); - void Log(const char *Format, ...); -} // namespace Logger diff --git a/Include/StringUtil.hpp b/Include/StringUtil.hpp deleted file mode 100644 index 39c2c02..0000000 --- a/Include/StringUtil.hpp +++ /dev/null @@ -1,20 +0,0 @@ -#pragma once -#include - -namespace StringUtil -{ - enum class DateFormat - { - YearMonthDay, - YearDayMonth - }; - - // 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); - // Gets date string. Asc is default if nothing passed. - std::string GetDateString(StringUtil::DateFormat Format = StringUtil::DateFormat::YearMonthDay); -} // namespace StringUtil diff --git a/Include/Strings.hpp b/Include/Strings.hpp deleted file mode 100644 index 6bcdb6b..0000000 --- a/Include/Strings.hpp +++ /dev/null @@ -1,30 +0,0 @@ -#pragma once -#include - -namespace Strings -{ - // Attempts to load strings from file in RomFS. - bool Initialize(void); - // Returns string with name and index. Returns nullptr if string doesn't exist. - const char *GetByName(std::string_view Name, int Index); - // Names of strings to prevent typos. - namespace Names - { - static constexpr std::string_view TranslationInfo = "TranslationInfo"; - static constexpr std::string_view ControlGuides = "ControlGuides"; - static constexpr std::string_view SaveDataTypes = "SaveDataTypes"; - static constexpr std::string_view MainMenuNames = "MainMenuNames"; - static constexpr std::string_view SettingsMenu = "SettingsMenu"; - static constexpr std::string_view ExtrasMenu = "ExtrasMenu"; - static constexpr std::string_view YesNo = "YesNo"; - static constexpr std::string_view HoldingStrings = "HoldingStrings"; - static constexpr std::string_view OnOff = "OnOff"; - static constexpr std::string_view BackupMenu = "BackupMenu"; - static constexpr std::string_view CopyingFiles = "CopyingFiles"; - static constexpr std::string_view BackupMenuConfirmations = "BackupMenuConfirmations"; - static constexpr std::string_view DeletingFiles = "DeletingFiles"; - static constexpr std::string_view KeyboardStrings = "KeyboardStrings"; - static constexpr std::string_view UserOptions = "UserOptions"; - static constexpr std::string_view CreatingSaveDataFor = "CreatingSaveDataFor"; - } // namespace Names -} // namespace Strings diff --git a/Include/System/ProgressTask.hpp b/Include/System/ProgressTask.hpp deleted file mode 100644 index 53a6f8e..0000000 --- a/Include/System/ProgressTask.hpp +++ /dev/null @@ -1,27 +0,0 @@ -#pragma once -#include "System/Task.hpp" - -namespace System -{ - class ProgressTask : public System::Task - { - public: - template - ProgressTask(void (*Function)(System::ProgressTask *, Args...), Args... Arguments) - : System::Task(Function, this, std::forward(Arguments)...){}; - - // Resets current down to 0 and sets goal - void Reset(double Goal); - // Sets current value - void UpdateCurrent(double Current); - - // Returns goal. - double GetGoal(void) const; - // Returns progress - double GetCurrentProgress(void) const; - - private: - // Current value and goal - double m_Current, m_Goal; - }; -} // namespace System diff --git a/Include/System/Task.hpp b/Include/System/Task.hpp deleted file mode 100644 index 5e615f1..0000000 --- a/Include/System/Task.hpp +++ /dev/null @@ -1,46 +0,0 @@ -#pragma once -#include -#include -#include - -namespace System -{ - class Task - { - public: - template - Task(void (*Function)(System::Task *, Args...), Args... Arguments) - { - m_Thread = std::thread(Function, this, std::forward(Arguments)...); - } - - // This is an alternate constructor that passes through a pointer to a derived class. - template - Task(void (*Function)(TaskType *, Args...), TaskType *Task, Args... Arguments) - { - m_Thread = std::thread(Function, Task, std::forward(Arguments)...); - } - - virtual ~Task(); - - // Returns if thread is still running. - bool IsRunning(void) const; - // Signals thread is finished running. - void Finished(void); - - // Sets status to display. - void SetStatus(const char *Format, ...); - // Returns status string. - std::string GetStatus(void); - - private: - // Whether task is still running. - bool m_IsRunning = true; - // Status string the thread can set that the main thread can display. - std::string m_Status; - // Mutex so that string doesn't get messed up. - std::mutex m_StatusLock; - // Thread - std::thread m_Thread; - }; -} // namespace System diff --git a/Include/System/Timer.hpp b/Include/System/Timer.hpp deleted file mode 100644 index d4b8a3c..0000000 --- a/Include/System/Timer.hpp +++ /dev/null @@ -1,22 +0,0 @@ -#pragma once -#include - -namespace System -{ - class Timer - { - public: - Timer(uint64_t TriggerTicks); - - // Returns if timer was triggered. Automatically restarts time. - bool IsTriggered(void); - // Manually restarts timer. - void Restart(void); - - private: - // Beginning ticks. - uint64_t m_StartingTicks; - // How many ticks to trigger the timer. - uint64_t m_TriggerTicks; - }; -} // namespace System diff --git a/Include/UI/ColorMod.hpp b/Include/UI/ColorMod.hpp deleted file mode 100644 index b9847d2..0000000 --- a/Include/UI/ColorMod.hpp +++ /dev/null @@ -1,23 +0,0 @@ -#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 deleted file mode 100644 index 0bdc53e..0000000 --- a/Include/UI/Element.hpp +++ /dev/null @@ -1,15 +0,0 @@ -#pragma once -#include "SDL.hpp" - -namespace UI -{ - class Element - { - public: - Element(void) = default; - virtual ~Element() {}; - - virtual void Update(bool HasFocus) = 0; - virtual void Render(SDL_Texture *Target, bool HasFocus) = 0; - }; -} // namespace UI diff --git a/Include/UI/IconMenu.hpp b/Include/UI/IconMenu.hpp deleted file mode 100644 index 9220084..0000000 --- a/Include/UI/IconMenu.hpp +++ /dev/null @@ -1,29 +0,0 @@ -#pragma once -#include "UI/Menu.hpp" - -namespace UI -{ - // This is a really hard-coded, hacky way of using the Menu's code with icons as options. - class IconMenu : public UI::Menu - { - public: - // Default constructor - IconMenu(void) = default; - // This calls initialize. - IconMenu(int X, int Y, int RenderTargetHeight); - ~IconMenu() {}; - - // Initializes menu with values passed. - void Initialize(int X, int Y, int RenderTargetHeight); - - void Update(bool HasFocus); - void Render(SDL_Texture *Target, bool HasFocus); - - // Adds icon to menu. - void AddOption(SDL::SharedTexture NewOption); - - private: - // Vector of option icons. - std::vector m_Options; - }; -} // namespace UI diff --git a/Include/UI/Menu.hpp b/Include/UI/Menu.hpp deleted file mode 100644 index 8dc22e3..0000000 --- a/Include/UI/Menu.hpp +++ /dev/null @@ -1,57 +0,0 @@ -#pragma once -#include "SDL.hpp" -#include "UI/ColorMod.hpp" -#include "UI/Element.hpp" -#include -#include - -namespace UI -{ - class Menu : public UI::Element - { - public: - Menu(int X, int Y, int Width, int FontSize, int RenderTargetHeight); - ~Menu() {}; - - void Update(bool HasFocus); - void Render(SDL_Texture *Target, bool HasFocus); - - void AddOption(std::string_view NewOption); - int GetSelected(void) const; - void SetSelected(int Selected); - // This is a workaround until I figure out a better solution that won't be a ton of work and time. - void SetWidth(int Width); - - void Reset(void); - - protected: - // X and Y coordinates. - double m_X, m_Y; - // Selected option. - int m_Selected = 0; - // Color pulse - UI::ColorMod m_ColorMod; - // Calculated height of the options. - int m_OptionHeight; - // Render target for options so they can't render outside the bounding area. - SDL::SharedTexture m_OptionTarget = nullptr; - - private: - // Need to preserve the original Y. - double m_OriginalY; - // This is the TargetY the menu catches up to so-to-speak. - double m_TargetY; - // This is calculated according to the render target's length. - int m_ScrollLength; - // Maximum display length of options. - int m_Width; - // Font size. - int m_FontSize; - // Vertical size of the destination render target in pixels. - int m_RenderTargetHeight; - // Maximum number of display options render target can show. - int m_MaxDisplayOptions; - // Vector of options. - std::vector m_Options; - }; -} // namespace UI diff --git a/Include/UI/RenderFunctions.hpp b/Include/UI/RenderFunctions.hpp deleted file mode 100644 index 541918c..0000000 --- a/Include/UI/RenderFunctions.hpp +++ /dev/null @@ -1,9 +0,0 @@ -#pragma once -#include "SDL.hpp" - -// These are just functions to render generic parts of the UI. -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, uint8_t ColorMod); -} // namespace UI diff --git a/Include/UI/SlideOutPanel.hpp b/Include/UI/SlideOutPanel.hpp deleted file mode 100644 index 316264f..0000000 --- a/Include/UI/SlideOutPanel.hpp +++ /dev/null @@ -1,54 +0,0 @@ -#pragma once -#include "SDL.hpp" -#include "UI/Element.hpp" -#include - -namespace UI -{ - class SlideOutPanel : public UI::Element - { - public: - // Which side the panel the slides out from. - enum class Side - { - Left, - Right - }; - // Width of panel, which side it "spawns" from. - SlideOutPanel(int Width, SlideOutPanel::Side Side); - ~SlideOutPanel() {}; - - void Update(bool HasFocus); - void Render(SDL_Texture *Target, bool HasFocus); - // Clears the panel target to a semi-transparent black. - void ClearTarget(void); - // Resets panel back to default values. - void Reset(void); - // Closes panel. - void Close(void); - // Returns whether or not panel is fully open. - bool IsOpen(void) const; - // Returns whether or not the panel is fully closed. - bool IsClosed(void) const; - // Adds new element to vector. - void PushNewElement(std::shared_ptr NewElement); - // Returns pointer to render target to allow rendering besides elements pushed. - SDL_Texture *Get(void); - - private: - // X coordinate - double m_X; - // Width of panel. - int m_Width; - // Side the panel spawned from. - SlideOutPanel::Side m_Side; - // Vector of elements. - std::vector> m_Elements; - // Bool for whether panel is open or not. - bool m_IsOpen = false; - // Whether or not to close panel. - bool m_ClosePanel = false; - // Render target - SDL::SharedTexture m_RenderTarget; - }; -} // namespace UI diff --git a/Include/UI/TitleTile.hpp b/Include/UI/TitleTile.hpp deleted file mode 100644 index 5804124..0000000 --- a/Include/UI/TitleTile.hpp +++ /dev/null @@ -1,28 +0,0 @@ -#pragma once -#include "SDL.hpp" - -namespace UI -{ - class TitleTile - { - public: - TitleTile(bool IsFavorite, SDL::SharedTexture Icon); - - void Update(bool IsSelected); - void Render(SDL_Texture *Target, int X, int Y); - - // Resets width and height. - void Reset(void); - // Returns RenderWidth and RenderHeight - int GetWidth(void) const; - int GetHeight(void) const; - - private: - // Width and height so icon can "expand" when highlighted. - int m_RenderWidth = 128, m_RenderHeight = 128; - // Whether or not the title is a favorite - bool m_IsFavorite = false; - // Icon - SDL::SharedTexture m_Icon = nullptr; - }; -} // namespace UI diff --git a/Include/UI/TitleView.hpp b/Include/UI/TitleView.hpp deleted file mode 100644 index ef55a1d..0000000 --- a/Include/UI/TitleView.hpp +++ /dev/null @@ -1,38 +0,0 @@ -#pragma once -#include "Data/Data.hpp" -#include "SDL.hpp" -#include "UI/ColorMod.hpp" -#include "UI/Element.hpp" -#include "UI/TitleTile.hpp" -#include - -namespace UI -{ - class TitleView : public UI::Element - { - public: - TitleView(Data::User *User); - void Update(bool HasFocus); - void Render(SDL_Texture *Target, bool HasFocus); - // Returns index of selected title. - int GetSelected(void) const; - // Refreshes the view using m_User - void Refresh(void); - // Resets all tiles to 128x128 - void Reset(void); - - private: - // Saves pointer to the user passed. - Data::User *m_User = nullptr; - // Y coordinate. - double m_Y = 28.0f; - // Currently highlighted/selected title. - int m_Selected = 0; - // This is to save the X and Y coordinates so the selected icon can be drawn last. - double m_SelectedX, m_SelectedY = 28.0f; - // Color mod for pulse. - UI::ColorMod m_ColorMod; - // Vector of tiles. - std::vector m_TitleTiles; - }; -} // namespace UI diff --git a/Libraries/FsLib b/Libraries/FsLib index c3a4149..2fec696 160000 --- a/Libraries/FsLib +++ b/Libraries/FsLib @@ -1 +1 @@ -Subproject commit c3a41490252faa9ca1d8f270fcdc338596d53ef6 +Subproject commit 2fec69606748149dc6048ef4d1b27f4baf37dda2 diff --git a/Libraries/SDLLib b/Libraries/SDLLib index eba5d13..fef8b09 160000 --- a/Libraries/SDLLib +++ b/Libraries/SDLLib @@ -1 +1 @@ -Subproject commit eba5d13d1c20c96d4e848f0ae1153e2740099213 +Subproject commit fef8b0927d92a97af47f9e554d4fe310c5de66d8 diff --git a/Makefile b/Makefile index 7fd60f1..a1ae85a 100644 --- a/Makefile +++ b/Makefile @@ -32,14 +32,14 @@ include $(DEVKITPRO)/libnx/switch_rules #--------------------------------------------------------------------------------- TARGET := JKSV BUILD := build -SOURCES := Source Source/AppStates Source/UI Source/Data Source/System Source/FS +SOURCES := source source/appstates source/ui source/data source/system source/fs DATA := data -INCLUDES := Include ./Libraries/FsLib/Switch/FsLib/include ./Libraries/SDLLib/SDL/include +INCLUDES := include ./Libraries/FsLib/Switch/FsLib/include ./Libraries/SDLLib/SDL/include EXEFS_SRC := exefs_src APP_TITLE := JKSV APP_AUTHOR := JK APP_VERSION := 12.22.2024 -ROMFS := RomFS +ROMFS := romfs ICON := icon.jpg #--------------------------------------------------------------------------------- @@ -155,12 +155,12 @@ $(BUILD): FsLib SDLLib #--------------------------------------------------------------------------------- FsLib: - @$(MAKE) -C ./Libraries/FsLib/Switch/FsLib/ + @$(MAKE) -C ./Libraries/FsLib/Switch/FsLib/ -j #--------------------------------------------------------------------------------- SDLLib: - @$(MAKE) -C ./Libraries/SDLLib/SDL/ + @$(MAKE) -C ./Libraries/SDLLib/SDL/ -j #--------------------------------------------------------------------------------- clean: diff --git a/Source/AppStates/BackupMenuState.cpp b/Source/AppStates/BackupMenuState.cpp deleted file mode 100644 index 2dd0e2e..0000000 --- a/Source/AppStates/BackupMenuState.cpp +++ /dev/null @@ -1,288 +0,0 @@ -#include "AppStates/BackupMenuState.hpp" -#include "AppStates/ConfirmState.hpp" -#include "AppStates/ProgressState.hpp" -#include "Colors.hpp" -#include "Config.hpp" -#include "FS/FileIO.hpp" -#include "FS/SaveMount.hpp" -#include "FS/ZipIO.hpp" -#include "FsLib.hpp" -#include "Input.hpp" -#include "JKSV.hpp" -#include "Keyboard.hpp" -#include "Logger.hpp" -#include "SDL.hpp" -#include "StringUtil.hpp" -#include "Strings.hpp" -#include "System/ProgressTask.hpp" -#include "System/Task.hpp" -#include - -// This struct is used to pass data to Restore, Delete, and upload. -struct TargetStruct -{ - FsLib::Path TargetPath; - uint64_t JournalSize = 0; - BackupMenuState *CreatingState = nullptr; -}; - -// This is the function to create new backups. -static void CreateNewBackup(System::ProgressTask *Task, FsLib::Path DestinationPath, BackupMenuState *CreatingState) -{ - // This extension search is lazy and needs to be revised. - if (Config::GetByKey(Config::Keys::ExportToZip) || std::strstr(DestinationPath.CString(), ".zip") != NULL) - { - zipFile NewBackup = zipOpen64(DestinationPath.CString(), APPEND_STATUS_CREATE); - FS::CopyDirectoryToZip(FS::DEFAULT_SAVE_PATH, NewBackup, Task); - zipClose(NewBackup, NULL); - } - else - { - FS::CopyDirectory(FS::DEFAULT_SAVE_PATH, DestinationPath, 0, {}, Task); - } - CreatingState->RefreshListing(); - Task->Finished(); -} - -static void RestoreBackup(System::ProgressTask *Task, std::shared_ptr DataStruct) -{ - // Wipe the save root first. - if (!FsLib::DeleteDirectoryRecursively(FS::DEFAULT_SAVE_PATH)) - { - Logger::Log("Error restoring save. Unable to reset save data: %s", FsLib::GetErrorString()); - Task->Finished(); - return; - } - - if (FsLib::DirectoryExists(DataStruct->TargetPath)) - { - FS::CopyDirectory(DataStruct->TargetPath, FS::DEFAULT_SAVE_PATH, DataStruct->JournalSize, FS::DEFAULT_SAVE_MOUNT, Task); - } - else if (std::strstr(DataStruct->TargetPath.CString(), ".zip") != NULL) - { - unzFile TargetZip = unzOpen64(DataStruct->TargetPath.CString()); - if (!TargetZip) - { - Logger::Log("Error opening zip for reading."); - Task->Finished(); - return; - } - FS::CopyZipToDirectory(TargetZip, FS::DEFAULT_SAVE_PATH, DataStruct->JournalSize, FS::DEFAULT_SAVE_MOUNT, Task); - unzClose(TargetZip); - } - else - { - FS::CopyFile(DataStruct->TargetPath, FS::DEFAULT_SAVE_PATH, DataStruct->JournalSize, FS::DEFAULT_SAVE_MOUNT, Task); - } - Task->Finished(); -} - -static void DeleteBackup(System::Task *Task, std::shared_ptr DataStruct) -{ - if (Task) - { - Task->SetStatus(Strings::GetByName(Strings::Names::DeletingFiles, 0), DataStruct->TargetPath.CString()); - } - - if (FsLib::DirectoryExists(DataStruct->TargetPath) && !FsLib::DeleteDirectoryRecursively(DataStruct->TargetPath)) - { - Logger::Log("Error deleting folder backup: %s", FsLib::GetErrorString()); - } - else if (!FsLib::DeleteFile(DataStruct->TargetPath)) - { - Logger::Log("Error deleting backup: %s", FsLib::GetErrorString()); - } - DataStruct->CreatingState->RefreshListing(); - Task->Finished(); -} - -BackupMenuState::BackupMenuState(Data::User *User, Data::TitleInfo *TitleInfo, FsSaveDataType SaveType) - : m_User(User), m_TitleInfo(TitleInfo), m_SaveType(SaveType), - m_DirectoryPath(Config::GetWorkingDirectory() / m_TitleInfo->GetPathSafeTitle()), - m_TitleWidth(SDL::Text::GetWidth(22, m_TitleInfo->GetTitle())), m_TitleTimer(3000) -{ - if (!m_Initialized) - { - m_PanelWidth = SDL::Text::GetWidth(22, Strings::GetByName(Strings::Names::ControlGuides, 2)) + 64; - // To do: Give classes an alternate so they don't have to be constructed. - m_BackupMenu = std::make_unique(8, 8, m_PanelWidth - 24, 24, 600); - m_SlidePanel = std::make_unique(m_PanelWidth, UI::SlideOutPanel::Side::Right); - m_MenuTarget = - SDL::TextureManager::CreateLoadTexture("BackupMenuTarget", m_PanelWidth, 600, SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET); - m_Initialized = true; - } - - if (m_TitleWidth >= m_PanelWidth) - { - m_ScrollTitle = true; - m_TitleX = 8; - } - else - { - m_TitleX = (m_PanelWidth / 2) - (m_TitleWidth / 2); - } - BackupMenuState::RefreshListing(); -} - -void BackupMenuState::Update(void) -{ - if (Input::ButtonPressed(HidNpadButton_A) && m_BackupMenu->GetSelected() == 0) - { - // Get name for backup. - char BackupName[0x81] = {0}; - - // Set backup to default. - std::snprintf(BackupName, 0x64, "%s - %s", m_User->GetPathSafeNickname(), StringUtil::GetDateString().c_str()); - - if (!Input::ButtonHeld(HidNpadButton_ZR) && - !Keyboard::GetInput(SwkbdType_QWERTY, BackupName, Strings::GetByName(Strings::Names::KeyboardStrings, 0), BackupName, 0x80)) - { - return; - } - // To do: This isn't a good way to check for this... Check to make sure zip has zip extension. - if (Config::GetByKey(Config::Keys::ExportToZip) && std::strstr(BackupName, ".zip") == NULL) - { - // To do: I should check this. - std::strcat(BackupName, ".zip"); - } - else if (!Config::GetByKey(Config::Keys::ExportToZip) && !std::strstr(BackupName, ".zip") && - !FsLib::DirectoryExists(m_DirectoryPath / BackupName) && !FsLib::CreateDirectory(m_DirectoryPath / BackupName)) - { - return; - } - // Push the task. - JKSV::PushState(std::make_shared(CreateNewBackup, m_DirectoryPath / BackupName, this)); - } - else if (Input::ButtonPressed(HidNpadButton_Y) && m_BackupMenu->GetSelected() > 0 && - (m_SaveType != FsSaveDataType_System || Config::GetByKey(Config::Keys::AllowSystemSaveWriting))) - { - int Selected = m_BackupMenu->GetSelected() - 1; - - std::shared_ptr DataStruct(new TargetStruct); - DataStruct->TargetPath = m_DirectoryPath / m_DirectoryListing[Selected]; - DataStruct->JournalSize = m_TitleInfo->GetJournalSize(m_SaveType); - - std::string QueryString = - StringUtil::GetFormattedString(Strings::GetByName(Strings::Names::BackupMenuConfirmations, 0), m_DirectoryListing[Selected]); - - JKSV::PushState(std::make_shared>( - QueryString, - Config::GetByKey(Config::Keys::HoldForRestoration), - RestoreBackup, - DataStruct)); - } - else if (Input::ButtonPressed(HidNpadButton_X) && m_BackupMenu->GetSelected() > 0) - { - // Selected needs to be offset by one to account for New - int Selected = m_BackupMenu->GetSelected() - 1; - - // Create struct to pass. - std::shared_ptr DataStruct(new TargetStruct); - DataStruct->TargetPath = m_DirectoryPath / m_DirectoryListing[Selected]; - DataStruct->CreatingState = this; - - // Get the string. - std::string QueryString = - StringUtil::GetFormattedString(Strings::GetByName(Strings::Names::BackupMenuConfirmations, 1), m_DirectoryListing[Selected]); - - // Create/push new state. - JKSV::PushState(std::make_shared>(QueryString, - Config::GetByKey(Config::Keys::HoldForDeletion), - DeleteBackup, - DataStruct)); - } - else if (Input::ButtonPressed(HidNpadButton_B)) - { - FsLib::CloseFileSystem(FS::DEFAULT_SAVE_MOUNT); - m_SlidePanel->Close(); - } - else if (m_SlidePanel->IsClosed()) - { - m_SlidePanel->Reset(); - AppState::Deactivate(); - } - - m_SlidePanel->Update(AppState::HasFocus()); - // This state bypasses the Slideout panel's normal behavior because it kind of has to. - m_BackupMenu->Update(AppState::HasFocus()); -} - -void BackupMenuState::Render(void) -{ - // Clear panel target. - m_SlidePanel->ClearTarget(); - // Render the current title's name. - BackupMenuState::RenderTitle(); - SDL::RenderLine(m_SlidePanel->Get(), 10, 42, m_PanelWidth - 20, 42, Colors::White); - SDL::RenderLine(m_SlidePanel->Get(), 10, 648, m_PanelWidth - 20, 648, Colors::White); - SDL::Text::Render(m_SlidePanel->Get(), - 32, - 673, - 22, - SDL::Text::NO_TEXT_WRAP, - Colors::White, - Strings::GetByName(Strings::Names::ControlGuides, 2)); - - // Clear menu target. - m_MenuTarget->Clear(Colors::Transparent); - // Render menu to it. - m_BackupMenu->Render(m_MenuTarget->Get(), AppState::HasFocus()); - // Render it to panel target. - m_MenuTarget->Render(m_SlidePanel->Get(), 0, 43); - m_SlidePanel->Render(NULL, AppState::HasFocus()); -} - -void BackupMenuState::RefreshListing(void) -{ - m_DirectoryListing.Open(m_DirectoryPath); - if (!m_DirectoryListing.IsOpen()) - { - return; - } - - m_BackupMenu->Reset(); - m_BackupMenu->AddOption(Strings::GetByName(Strings::Names::BackupMenu, 0)); - for (int64_t i = 0; i < m_DirectoryListing.GetEntryCount(); i++) - { - m_BackupMenu->AddOption(m_DirectoryListing[i]); - } -} - -void BackupMenuState::RenderTitle(void) -{ - SDL_Texture *SlidePanelTarget = m_SlidePanel->Get(); - - if (m_ScrollTitle && m_ScrollTriggered && m_TitleX > -(m_TitleWidth + 8)) - { - m_TitleX -= 2; - } - else if (m_ScrollTitle && m_ScrollTriggered && m_TitleX <= -(m_TitleWidth + 8)) - { - m_TitleX = 8; - m_ScrollTriggered = false; - m_TitleTimer.Restart(); - } - else if (m_ScrollTitle && m_TitleTimer.IsTriggered()) - { - m_ScrollTriggered = true; - } - - if (m_ScrollTitle && m_ScrollTriggered) - { - // This is just a trick, or maybe the only way to accomplish this. Either way, it works. - // Render title first time. - SDL::Text::Render(SlidePanelTarget, m_TitleX, 8, 22, SDL::Text::NO_TEXT_WRAP, Colors::White, m_TitleInfo->GetTitle()); - // Render it again following the first. - SDL::Text::Render(SlidePanelTarget, - m_TitleX + m_TitleWidth + 16, - 8, - 22, - SDL::Text::NO_TEXT_WRAP, - Colors::White, - m_TitleInfo->GetTitle()); - } - else - { - SDL::Text::Render(SlidePanelTarget, m_TitleX, 8, 22, SDL::Text::NO_TEXT_WRAP, Colors::White, m_TitleInfo->GetTitle()); - } -} diff --git a/Source/AppStates/ExtrasMenuState.cpp b/Source/AppStates/ExtrasMenuState.cpp deleted file mode 100644 index 8f26fb6..0000000 --- a/Source/AppStates/ExtrasMenuState.cpp +++ /dev/null @@ -1,39 +0,0 @@ -#include "AppStates/ExtrasMenuState.hpp" -#include "Colors.hpp" -#include "Input.hpp" -#include "Strings.hpp" -#include - -namespace -{ - constexpr std::string_view SECONDARY_TARGET = "SecondaryTarget"; -} - -ExtrasMenuState::ExtrasMenuState(void) - : m_ExtrasMenu(32, 8, 1000, 24, 555), - m_RenderTarget(SDL::TextureManager::CreateLoadTexture(SECONDARY_TARGET, 1080, 555, SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET)) -{ - const char *ExtrasString = nullptr; - int CurrentString = 0; - while ((ExtrasString = Strings::GetByName(Strings::Names::ExtrasMenu, CurrentString++)) != nullptr) - { - m_ExtrasMenu.AddOption(ExtrasString); - } -} - -void ExtrasMenuState::Update(void) -{ - m_ExtrasMenu.Update(AppState::HasFocus()); - - if (Input::ButtonPressed(HidNpadButton_B)) - { - AppState::Deactivate(); - } -} - -void ExtrasMenuState::Render(void) -{ - m_RenderTarget->Clear(Colors::Transparent); - m_ExtrasMenu.Render(m_RenderTarget->Get(), AppState::HasFocus()); - m_RenderTarget->Render(NULL, 201, 91); -} diff --git a/Source/AppStates/MainMenuState.cpp b/Source/AppStates/MainMenuState.cpp deleted file mode 100644 index 4cb9642..0000000 --- a/Source/AppStates/MainMenuState.cpp +++ /dev/null @@ -1,100 +0,0 @@ -#include "AppStates/MainMenuState.hpp" -#include "AppStates/ExtrasMenuState.hpp" -#include "AppStates/SettingsState.hpp" -#include "AppStates/TextTitleSelectState.hpp" -#include "AppStates/TitleSelectCommon.hpp" -#include "AppStates/TitleSelectState.hpp" -#include "AppStates/UserOptionState.hpp" -#include "Colors.hpp" -#include "Config.hpp" -#include "Input.hpp" -#include "JKSV.hpp" -#include "Logger.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_MainMenu(50, 15, 555), - m_ControlGuide(Strings::GetByName(Strings::Names::ControlGuides, 0)), m_ControlGuideX(1220 - SDL::Text::GetWidth(22, m_ControlGuide)) -{ - // Fetch user list. - Data::GetUsers(m_Users); - - // Loop through add user's icon to menu and create states. - for (size_t i = 0; i < m_Users.size(); i++) - { - m_MainMenu.AddOption(m_Users.at(i)->GetSharedIcon()); - - if (Config::GetByKey(Config::Keys::JKSMTextMode)) - { - m_States.push_back(std::make_shared(m_Users.at(i))); - } - else - { - m_States.push_back(std::make_shared(m_Users.at(i))); - } - } - // Add the settings and extras. - m_States.push_back(std::make_shared()); - m_States.push_back(std::make_shared()); - - // Create icons for the other two. - m_SettingsIcon = SDL::TextureManager::CreateLoadTexture("SettingsIcon", "romfs:/Textures/SettingsIcon.png"); - m_ExtrasIcon = SDL::TextureManager::CreateLoadTexture("ExtrasIcon", "romfs:/Textures/ExtrasIcon.png"); - - // Finally add them to the end. - m_MainMenu.AddOption(m_SettingsIcon); - m_MainMenu.AddOption(m_ExtrasIcon); -} - -void MainMenuState::Update(void) -{ - m_MainMenu.Update(AppState::HasFocus()); - - if (Input::ButtonPressed(HidNpadButton_A) && m_MainMenu.GetSelected() < static_cast(m_Users.size()) && - m_Users.at(m_MainMenu.GetSelected())->GetTotalDataEntries() > 0) - { - m_States.at(m_MainMenu.GetSelected())->Reactivate(); - JKSV::PushState(m_States.at(m_MainMenu.GetSelected())); - } - else if (Input::ButtonPressed(HidNpadButton_A) && m_MainMenu.GetSelected() >= static_cast(m_Users.size())) - { - m_States.at(m_MainMenu.GetSelected())->Reactivate(); - JKSV::PushState(m_States.at(m_MainMenu.GetSelected())); - } - else if (Input::ButtonPressed(HidNpadButton_X) && m_MainMenu.GetSelected() < static_cast(m_Users.size())) - { - // Get pointers to data the user option state needs. - Data::User *TargetUser = m_Users.at(m_MainMenu.GetSelected()); - TitleSelectCommon *TargetTitleSelect = reinterpret_cast(m_States.at(m_MainMenu.GetSelected()).get()); - - JKSV::PushState(std::make_shared(TargetUser, TargetTitleSelect)); - } -} - -void MainMenuState::Render(void) -{ - // Clear render target by rendering background to it. - m_Background->Render(m_RenderTarget->Get(), 0, 0); - // Render menu. - m_MainMenu.Render(m_RenderTarget->Get(), AppState::HasFocus()); - // Render target to screen. - m_RenderTarget->Render(NULL, 0, 91); - - // Render next state for current user and control guide if this state has focus. - if (AppState::HasFocus()) - { - m_States.at(m_MainMenu.GetSelected())->Render(); - SDL::Text::Render(NULL, m_ControlGuideX, 673, 22, SDL::Text::NO_TEXT_WRAP, Colors::White, m_ControlGuide); - } -} - -void MainMenuState::RefreshViewStates(void) -{ - for (size_t i = 0; i < m_Users.size(); i++) - { - m_Users.at(i)->SortData(); - std::static_pointer_cast(m_States.at(i))->Refresh(); - } -} diff --git a/Source/AppStates/ProgressState.cpp b/Source/AppStates/ProgressState.cpp deleted file mode 100644 index 2bd2c8a..0000000 --- a/Source/AppStates/ProgressState.cpp +++ /dev/null @@ -1,35 +0,0 @@ -#include "AppStates/ProgressState.hpp" -#include "Colors.hpp" -#include "SDL.hpp" -#include "StringUtil.hpp" -#include "Strings.hpp" -#include "UI/RenderFunctions.hpp" -#include - -#include "Input.hpp" - -void ProgressState::Update(void) -{ - if (!m_Task.IsRunning()) - { - AppState::Deactivate(); - } - - m_ProgressBarWidth = std::ceil(656.0f * m_Task.GetCurrentProgress()); - m_Progress = std::ceil(m_Task.GetCurrentProgress() * 100); - m_PercentageString = StringUtil::GetFormattedString("%u", m_Progress); - m_PerentageX = 640 - (SDL::Text::GetWidth(18, m_PercentageString.c_str())); -} - -void ProgressState::Render(void) -{ - // This will dim the background. - SDL::RenderRectFill(NULL, 0, 0, 1280, 720, Colors::BackgroundDim); - - // Render the dialog and little loading bar thingy. - UI::RenderDialogBox(NULL, 280, 262, 720, 256); - SDL::Text::Render(NULL, 312, 288, 18, 648, Colors::White, m_Task.GetStatus().c_str()); - SDL::RenderRectFill(NULL, 312, 462, 656, 32, Colors::Black); - SDL::RenderRectFill(NULL, 312, 462, m_ProgressBarWidth, 32, Colors::Green); - SDL::Text::Render(NULL, m_PerentageX, 468, 18, SDL::Text::NO_TEXT_WRAP, Colors::White, "%s%%", m_PercentageString.c_str()); -} diff --git a/Source/AppStates/SaveCreateState.cpp b/Source/AppStates/SaveCreateState.cpp deleted file mode 100644 index 74524c1..0000000 --- a/Source/AppStates/SaveCreateState.cpp +++ /dev/null @@ -1,131 +0,0 @@ -#include "AppStates/SaveCreateState.hpp" -#include "AppStates/TaskState.hpp" -#include "Data/Data.hpp" -#include "Input.hpp" -#include "JKSV.hpp" -#include "Logger.hpp" -#include "Strings.hpp" -#include "System/Task.hpp" -#include -#include -#include -#include - -// This sorts the vector alphabetically so stuff is easier to find -static bool CompareInfo(Data::TitleInfo *InfoA, Data::TitleInfo *InfoB) -{ - const char *TitleA = InfoA->GetTitle(); - const char *TitleB = InfoB->GetTitle(); - - size_t TitleALength = std::char_traits::length(TitleA); - size_t TitleBLength = std::char_traits::length(TitleB); - size_t ShortestTitle = TitleALength < TitleBLength ? TitleALength : TitleBLength; - // To do: This doesn't take into account which is the shortest title. This can still go out-of-bounds. - for (size_t i = 0, j = 0; i < ShortestTitle;) - { - uint32_t CodepointA = 0; - uint32_t CodepointB = 0; - - ssize_t UnitACount = decode_utf8(&CodepointA, reinterpret_cast(&TitleA[i])); - ssize_t UnitBCount = decode_utf8(&CodepointB, reinterpret_cast(&TitleB[j])); - - if (UnitACount <= 0 || UnitBCount <= 0) - { - return false; - } - - if (CodepointA != CodepointB) - { - return CodepointA < CodepointB; - } - - i += UnitACount; - j += UnitBCount; - } - return false; -} - -// This attempts to create the save data for the given user. It will fail if it already exists. -static void CreateSaveDataFor(System::Task *Task, Data::User *TargetUser, Data::TitleInfo *TitleInfo) -{ - // Attributes of save data. To do: Owner ID might not be the same. Need to research. - FsSaveDataAttribute SaveAttributes = {.application_id = TitleInfo->GetSaveDataOwnerID(), - .uid = TargetUser->GetAccountID(), - .system_save_data_id = 0, - .save_data_type = FsSaveDataType_Account, - .save_data_rank = FsSaveDataRank_Primary, - .save_data_index = 0}; - - // Creation info for save data. - FsSaveDataCreationInfo SaveCreationInfo = {.save_data_size = static_cast(TitleInfo->GetSaveDataSize(FsSaveDataType_Account)), - .journal_size = static_cast(TitleInfo->GetJournalSize(FsSaveDataType_Account)), - .available_size = 0x4000, - .owner_id = TitleInfo->GetSaveDataOwnerID(), - .flags = 0, - .save_data_space_id = FsSaveDataSpaceId_User}; - - // Save meta - FsSaveDataMetaInfo SaveMetaInfo = {.size = 0x40060, .type = FsSaveDataMetaType_Thumbnail}; - - // Set task status - Task->SetStatus(Strings::GetByName(Strings::Names::CreatingSaveDataFor, 0), TitleInfo->GetTitle()); - - Result FsError = fsCreateSaveDataFileSystem(&SaveAttributes, &SaveCreationInfo, &SaveMetaInfo); - if (R_FAILED(FsError)) - { - Logger::Log("Error creating save data for %016llX: 0x%X.", TitleInfo->GetSaveDataOwnerID(), FsError); - } - - Task->Finished(); -} - -SaveCreateState::SaveCreateState(Data::User *TargetUser, TitleSelectCommon *TitleSelect) - : m_User(TargetUser), m_TitleSelect(TitleSelect), m_SaveMenu(8, 8, 624, 22, 720) -{ - // If the panel is null, create it. - if (!m_SlidePanel) - { - // Create panel and menu. - m_SlidePanel = std::make_unique(640, UI::SlideOutPanel::Side::Right); - } - - // Get title info vector and copy titles to menu. - Data::GetTitleInfoByType(FsSaveDataType_Account, m_TitleInfoVector); - - // Sort it by alpha - std::sort(m_TitleInfoVector.begin(), m_TitleInfoVector.end(), CompareInfo); - - for (size_t i = 0; i < m_TitleInfoVector.size(); i++) - { - m_SaveMenu.AddOption(m_TitleInfoVector.at(i)->GetTitle()); - } -} - -void SaveCreateState::Update(void) -{ - m_SlidePanel->Update(AppState::HasFocus()); - m_SaveMenu.Update(AppState::HasFocus()); - - if (Input::ButtonPressed(HidNpadButton_A)) - { - Data::TitleInfo *TargetTitle = m_TitleInfoVector.at(m_SaveMenu.GetSelected()); - JKSV::PushState(std::make_shared(CreateSaveDataFor, m_User, TargetTitle)); - } - else if (Input::ButtonPressed(HidNpadButton_B)) - { - m_SlidePanel->Close(); - } - else if (m_SlidePanel->IsClosed()) - { - m_SlidePanel->Reset(); - AppState::Deactivate(); - } -} - -void SaveCreateState::Render(void) -{ - // Clear slide target, render menu, render slide to frame buffer. - m_SlidePanel->ClearTarget(); - m_SaveMenu.Render(m_SlidePanel->Get(), AppState::HasFocus()); - m_SlidePanel->Render(NULL, AppState::HasFocus()); -} diff --git a/Source/AppStates/SettingsState.cpp b/Source/AppStates/SettingsState.cpp deleted file mode 100644 index 0bd812b..0000000 --- a/Source/AppStates/SettingsState.cpp +++ /dev/null @@ -1,65 +0,0 @@ -#include "AppStates/SettingsState.hpp" -#include "Colors.hpp" -#include "Config.hpp" -#include "Input.hpp" -#include "StringUtil.hpp" -#include "Strings.hpp" - -namespace -{ - // All of these states share the same render target. - constexpr std::string_view SECONDARY_TARGET = "SecondaryTarget"; -} // namespace - -static inline const char *GetValueText(uint8_t Value) -{ - return Value == 1 ? Strings::GetByName(Strings::Names::OnOff, 0) : Strings::GetByName(Strings::Names::OnOff, 1); -} - -SettingsState::SettingsState(void) - : m_SettingsMenu(32, 8, 1000, 24, 555), - m_RenderTarget(SDL::TextureManager::CreateLoadTexture(SECONDARY_TARGET, 1080, 555, SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET)), - m_ControlGuideX(1220 - SDL::Text::GetWidth(22, Strings::GetByName(Strings::Names::ControlGuides, 3))) -{ - // Add the first two, because they don't have values to display. - m_SettingsMenu.AddOption(Strings::GetByName(Strings::Names::SettingsMenu, 0)); - m_SettingsMenu.AddOption(Strings::GetByName(Strings::Names::SettingsMenu, 1)); - - int CurrentString = 2; - const char *SettingsString = nullptr; - while (CurrentString < 16 && (SettingsString = Strings::GetByName(Strings::Names::SettingsMenu, CurrentString++)) != nullptr) - { - m_SettingsMenu.AddOption(StringUtil::GetFormattedString(SettingsString, GetValueText(Config::GetByIndex(CurrentString - 1)))); - } - // Add the scaling - m_SettingsMenu.AddOption( - StringUtil::GetFormattedString(Strings::GetByName(Strings::Names::SettingsMenu, 17), Config::GetAnimationScaling())); -} - -void SettingsState::Update(void) -{ - m_SettingsMenu.Update(AppState::HasFocus()); - - if (Input::ButtonPressed(HidNpadButton_B)) - { - AppState::Deactivate(); - } -} - -void SettingsState::Render(void) -{ - m_RenderTarget->Clear(Colors::Transparent); - m_SettingsMenu.Render(m_RenderTarget->Get(), AppState::HasFocus()); - m_RenderTarget->Render(NULL, 201, 91); - - if (AppState::HasFocus()) - { - SDL::Text::Render(NULL, - m_ControlGuideX, - 673, - 22, - SDL::Text::NO_TEXT_WRAP, - Colors::White, - Strings::GetByName(Strings::Names::ControlGuides, 3)); - } -} diff --git a/Source/AppStates/TaskState.cpp b/Source/AppStates/TaskState.cpp deleted file mode 100644 index 7efd4ff..0000000 --- a/Source/AppStates/TaskState.cpp +++ /dev/null @@ -1,23 +0,0 @@ -#include "AppStates/TaskState.hpp" -#include "Colors.hpp" -#include "SDL.hpp" - -void TaskState::Update(void) -{ - if (!m_Task.IsRunning()) - { - AppState::Deactivate(); - } -} - -void TaskState::Render(void) -{ - // Grab task string. - std::string Status = m_Task.GetStatus(); - // Center so it looks perty - int StatusX = 640 - (SDL::Text::GetWidth(24, Status.c_str()) / 2); - // Dim the background states. - SDL::RenderRectFill(NULL, 0, 0, 1280, 720, Colors::BackgroundDim); - // Render the status. - SDL::Text::Render(NULL, StatusX, 351, 24, SDL::Text::NO_TEXT_WRAP, Colors::White, Status.c_str()); -} diff --git a/Source/AppStates/TextTitleSelectState.cpp b/Source/AppStates/TextTitleSelectState.cpp deleted file mode 100644 index cc5ea18..0000000 --- a/Source/AppStates/TextTitleSelectState.cpp +++ /dev/null @@ -1,62 +0,0 @@ -#include "AppStates/TextTitleSelectState.hpp" -#include "AppStates/MainMenuState.hpp" -#include "Colors.hpp" -#include "Config.hpp" -#include "Input.hpp" -#include "SDL.hpp" -#include - -namespace -{ - constexpr std::string_view SECONDARY_TARGET = "SecondaryTarget"; -} - -TextTitleSelectState::TextTitleSelectState(Data::User *User) - : TitleSelectCommon(), m_User(User), m_TitleSelectMenu(32, 8, 1000, 20, 555), - m_RenderTarget(SDL::TextureManager::CreateLoadTexture(SECONDARY_TARGET, 1080, 555, SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET)) -{ - TextTitleSelectState::Refresh(); -} - -void TextTitleSelectState::Update(void) -{ - m_TitleSelectMenu.Update(AppState::HasFocus()); - - if (Input::ButtonPressed(HidNpadButton_Y)) - { - Config::AddRemoveFavorite(m_User->GetApplicationIDAt(m_TitleSelectMenu.GetSelected())); - MainMenuState::RefreshViewStates(); - } - else if (Input::ButtonPressed(HidNpadButton_B)) - { - AppState::Deactivate(); - } -} - -void TextTitleSelectState::Render(void) -{ - m_RenderTarget->Clear(Colors::Transparent); - m_TitleSelectMenu.Render(m_RenderTarget->Get(), AppState::HasFocus()); - TitleSelectCommon::RenderControlGuide(); - m_RenderTarget->Render(NULL, 201, 91); -} - -void TextTitleSelectState::Refresh(void) -{ - m_TitleSelectMenu.Reset(); - for (size_t i = 0; i < m_User->GetTotalDataEntries(); i++) - { - std::string Option; - uint64_t ApplicationID = m_User->GetApplicationIDAt(i); - const char *Title = Data::GetTitleInfoByID(ApplicationID)->GetTitle(); - if (Config::IsFavorite(ApplicationID)) - { - Option = std::string("^\uE017^ ") + Title; - } - else - { - Option = Title; - } - m_TitleSelectMenu.AddOption(Option.c_str()); - } -} diff --git a/Source/AppStates/TitleSelectCommon.cpp b/Source/AppStates/TitleSelectCommon.cpp deleted file mode 100644 index 622a322..0000000 --- a/Source/AppStates/TitleSelectCommon.cpp +++ /dev/null @@ -1,26 +0,0 @@ -#include "AppStates/TitleSelectCommon.hpp" -#include "Colors.hpp" -#include "SDL.hpp" -#include "Strings.hpp" - -TitleSelectCommon::TitleSelectCommon(void) -{ - if (m_TitleControlsX == 0) - { - m_TitleControlsX = 1220 - SDL::Text::GetWidth(22, Strings::GetByName(Strings::Names::ControlGuides, 1)); - } -} - -void TitleSelectCommon::RenderControlGuide(void) -{ - if (AppState::HasFocus()) - { - SDL::Text::Render(NULL, - m_TitleControlsX, - 673, - 22, - SDL::Text::NO_TEXT_WRAP, - Colors::White, - Strings::GetByName(Strings::Names::ControlGuides, 1)); - } -} diff --git a/Source/AppStates/TitleSelectState.cpp b/Source/AppStates/TitleSelectState.cpp deleted file mode 100644 index 76fbc2b..0000000 --- a/Source/AppStates/TitleSelectState.cpp +++ /dev/null @@ -1,69 +0,0 @@ -#include "AppStates/TitleSelectState.hpp" -#include "AppStates/BackupMenuState.hpp" -#include "AppStates/MainMenuState.hpp" -#include "Colors.hpp" -#include "Config.hpp" -#include "FS/SaveMount.hpp" -#include "FsLib.hpp" -#include "Input.hpp" -#include "JKSV.hpp" -#include "SDL.hpp" -#include "Strings.hpp" -#include - -namespace -{ - // All of these states share the same render target. - constexpr std::string_view SECONDARY_TARGET = "SecondaryTarget"; -} // namespace - -TitleSelectState::TitleSelectState(Data::User *User) - : TitleSelectCommon(), m_User(User), - m_RenderTarget(SDL::TextureManager::CreateLoadTexture(SECONDARY_TARGET, 1080, 555, SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET)), - m_TitleView(m_User) {}; - -void TitleSelectState::Update(void) -{ - m_TitleView.Update(AppState::HasFocus()); - - if (Input::ButtonPressed(HidNpadButton_A)) - { - // Get data needed to mount save. - uint64_t ApplicationID = m_User->GetApplicationIDAt(m_TitleView.GetSelected()); - FsSaveDataInfo *SaveInfo = m_User->GetSaveInfoByID(ApplicationID); - Data::TitleInfo *TitleInfo = Data::GetTitleInfoByID(ApplicationID); - - // Path to output to. - FsLib::Path TargetPath = Config::GetWorkingDirectory() / TitleInfo->GetPathSafeTitle(); - - if ((FsLib::DirectoryExists(TargetPath) || FsLib::CreateDirectory(TargetPath)) && FS::MountSaveData(*SaveInfo, FS::DEFAULT_SAVE_MOUNT)) - { - JKSV::PushState(std::make_shared(m_User, TitleInfo, static_cast(SaveInfo->save_data_type))); - } - } - else if (Input::ButtonPressed(HidNpadButton_B)) - { - // This will reset all the tiles so they're 128x128. - m_TitleView.Reset(); - AppState::Deactivate(); - } - else if (Input::ButtonPressed(HidNpadButton_Y)) - { - Config::AddRemoveFavorite(m_User->GetApplicationIDAt(m_TitleView.GetSelected())); - // MainMenuState has all the Users and views, so have it refresh. - MainMenuState::RefreshViewStates(); - } -} - -void TitleSelectState::Render(void) -{ - m_RenderTarget->Clear(Colors::Transparent); - m_TitleView.Render(m_RenderTarget->Get(), AppState::HasFocus()); - TitleSelectCommon::RenderControlGuide(); - m_RenderTarget->Render(NULL, 201, 91); -} - -void TitleSelectState::Refresh(void) -{ - m_TitleView.Refresh(); -} diff --git a/Source/AppStates/UserOptionState.cpp b/Source/AppStates/UserOptionState.cpp deleted file mode 100644 index 60bb6c7..0000000 --- a/Source/AppStates/UserOptionState.cpp +++ /dev/null @@ -1,137 +0,0 @@ -#include "AppStates/UserOptionState.hpp" -#include "AppStates/ProgressState.hpp" -#include "AppStates/SaveCreateState.hpp" -#include "Config.hpp" -#include "Data/Data.hpp" -#include "FS/FileIO.hpp" -#include "FS/SaveMount.hpp" -#include "FS/ZipIO.hpp" -#include "FsLib.hpp" -#include "Input.hpp" -#include "JKSV.hpp" -#include "Logger.hpp" -#include "StringUtil.hpp" -#include "Strings.hpp" -#include "System/ProgressTask.hpp" - -static void BackupAllForUser(System::ProgressTask *Task, Data::User *TargetUser) -{ - for (size_t i = 0; i < TargetUser->GetTotalDataEntries(); i++) - { - // This should be safe like this.... - FsSaveDataInfo *CurrentSaveInfo = TargetUser->GetSaveInfoAt(i); - Data::TitleInfo *CurrentTitle = Data::GetTitleInfoByID(CurrentSaveInfo->application_id); - - if (!CurrentSaveInfo || !CurrentTitle) - { - Logger::Log("One of these is nullptr?"); - continue; - } - - // Try to create target game folder. - FsLib::Path GameFolder = Config::GetWorkingDirectory() / CurrentTitle->GetPathSafeTitle(); - if (!FsLib::DirectoryExists(GameFolder) && !FsLib::CreateDirectory(GameFolder)) - { - Logger::Log("Error creating target game folder: %s", FsLib::GetErrorString()); - continue; - } - - // Try to mount save data. - bool SaveMounted = FS::MountSaveData(*CurrentSaveInfo, FS::DEFAULT_SAVE_MOUNT); - - if (CurrentTitle && SaveMounted && Config::GetByKey(Config::Keys::ExportToZip)) - { - FsLib::Path TargetPath = Config::GetWorkingDirectory() / CurrentTitle->GetPathSafeTitle() / TargetUser->GetPathSafeNickname() + - " - " + StringUtil::GetDateString() + ".zip"; - - zipFile TargetZip = zipOpen64(TargetPath.CString(), APPEND_STATUS_CREATE); - if (!TargetZip) - { - Logger::Log("Error creating zip: %s", FsLib::GetErrorString()); - continue; - } - FS::CopyDirectoryToZip(FS::DEFAULT_SAVE_PATH, TargetZip, Task); - zipClose(TargetZip, NULL); - } - else if (CurrentTitle && SaveMounted) - { - FsLib::Path TargetPath = Config::GetWorkingDirectory() / CurrentTitle->GetPathSafeTitle() / TargetUser->GetPathSafeNickname() + - " - " + StringUtil::GetDateString(); - - if (!FsLib::CreateDirectory(TargetPath)) - { - Logger::Log("Error creating backup directory: %s", FsLib::GetErrorString()); - continue; - } - FS::CopyDirectory(FS::DEFAULT_SAVE_PATH, TargetPath, 0, {}, Task); - } - - if (SaveMounted) - { - FsLib::CloseFileSystem(FS::DEFAULT_SAVE_MOUNT); - } - } - Task->Finished(); -} - -UserOptionState::UserOptionState(Data::User *User, TitleSelectCommon *TitleSelect) - : m_User(User), m_TitleSelect(TitleSelect), m_UserOptionMenu(8, 8, 460, 22, 720) -{ - // Check if panel needs to be created. It's shared by all instances. - if (!m_MenuPanel) - { - m_MenuPanel = std::make_unique(480, UI::SlideOutPanel::Side::Right); - } - - const char *CurrentString = nullptr; - int CurrentStringIndex = 0; - while ((CurrentString = Strings::GetByName(Strings::Names::UserOptions, CurrentStringIndex++)) != nullptr) - { - m_UserOptionMenu.AddOption(StringUtil::GetFormattedString(CurrentString, m_User->GetNickname())); - } -} - -void UserOptionState::Update(void) -{ - m_MenuPanel->Update(AppState::HasFocus()); - - if (Input::ButtonPressed(HidNpadButton_A)) - { - switch (m_UserOptionMenu.GetSelected()) - { - case 0: - { - JKSV::PushState(std::make_shared(BackupAllForUser, m_User)); - } - break; - - case 1: - { - JKSV::PushState(std::make_shared(m_User, m_TitleSelect)); - } - break; - } - } - else if (Input::ButtonPressed(HidNpadButton_B)) - { - m_MenuPanel->Close(); - } - else if (m_MenuPanel->IsClosed()) - { - AppState::Deactivate(); - m_MenuPanel->Reset(); - } - - m_UserOptionMenu.Update(AppState::HasFocus()); -} - -void UserOptionState::Render(void) -{ - // Render target user's title selection screen. - m_TitleSelect->Render(); - - // Render panel. - m_MenuPanel->ClearTarget(); - m_UserOptionMenu.Render(m_MenuPanel->Get(), AppState::HasFocus()); - m_MenuPanel->Render(NULL, AppState::HasFocus()); -} diff --git a/Source/Config.cpp b/Source/Config.cpp deleted file mode 100644 index 533c5b0..0000000 --- a/Source/Config.cpp +++ /dev/null @@ -1,232 +0,0 @@ -#include "Config.hpp" -#include "JSON.hpp" -#include "Logger.hpp" -#include "StringUtil.hpp" -#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"; - // Vector to preserve order now. - std::vector> s_ConfigVector; - // 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(std::strtoull(json_object_get_string(ArrayEntry), NULL, 16)); - } -} - -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_ConfigVector.push_back(std::make_pair(KeyName, json_object_get_uint64(ConfigValue))); - } - json_object_iter_next(&ConfigIterator); - } -} - -void Config::ResetToDefault(void) -{ - s_WorkingDirectory = "sdmc:/JKSV"; - s_ConfigVector.push_back(std::make_pair(Config::Keys::IncludeDeviceSaves.data(), 0)); - s_ConfigVector.push_back(std::make_pair(Config::Keys::AutoBackupOnRestore.data(), 1)); - s_ConfigVector.push_back(std::make_pair(Config::Keys::AutoNameBackups.data(), 1)); - s_ConfigVector.push_back(std::make_pair(Config::Keys::AutoUpload.data(), 1)); - s_ConfigVector.push_back(std::make_pair(Config::Keys::HoldForDeletion.data(), 0)); - s_ConfigVector.push_back(std::make_pair(Config::Keys::HoldForRestoration.data(), 0)); - s_ConfigVector.push_back(std::make_pair(Config::Keys::HoldForOverwrite.data(), 0)); - s_ConfigVector.push_back(std::make_pair(Config::Keys::OnlyListMountable.data(), 0)); - s_ConfigVector.push_back(std::make_pair(Config::Keys::ListAccountSystemSaves.data(), 0)); - s_ConfigVector.push_back(std::make_pair(Config::Keys::AllowSystemSaveWriting.data(), 0)); - s_ConfigVector.push_back(std::make_pair(Config::Keys::ExportToZip.data(), 0)); - s_ConfigVector.push_back(std::make_pair(Config::Keys::ZipCompressionLevel.data(), 6)); - s_ConfigVector.push_back(std::make_pair(Config::Keys::TitleSortType.data(), 0)); - s_ConfigVector.push_back(std::make_pair(Config::Keys::JKSMTextMode.data(), 0)); - s_ConfigVector.push_back(std::make_pair(Config::Keys::ForceEnglish.data(), 0)); - s_ConfigVector.push_back(std::make_pair(Config::Keys::EnableTrashBin.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_ConfigVector) - { - 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) - { - // Need to do it like this or json-c does decimal instead of hex. - json_object *NewFavorite = json_object_new_string(StringUtil::GetFormattedString("%016llX", TitleID).c_str()); - 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_string(StringUtil::GetFormattedString("%016llX", TitleID).c_str()); - 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) -{ - auto FindKey = std::find_if(s_ConfigVector.begin(), s_ConfigVector.end(), [Key](const std::pair &ConfigPair) { - return Key == ConfigPair.first; - }); - if (FindKey == s_ConfigVector.end()) - { - return 0; - } - return FindKey->second; -} - -uint8_t Config::GetByIndex(int Index) -{ - if (Index < 0 || Index >= static_cast(s_ConfigVector.size())) - { - return 0; - } - return s_ConfigVector.at(Index).second; -} - -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 deleted file mode 100644 index 330b9a1..0000000 --- a/Source/Data/Data.cpp +++ /dev/null @@ -1,194 +0,0 @@ -#include "Data/Data.hpp" -#include "Config.hpp" -#include "Data/AccountUID.hpp" -#include "FS/SaveMount.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(DeviceID, "Device", "romfs:/Textures/SystemSaves.png"))); - s_UserVector.push_back(std::make_pair(BCATID, Data::User(BCATID, "BCAT", "romfs:/Textures/BCAT.png"))); - s_UserVector.push_back(std::make_pair(CacheID, Data::User(CacheID, "Cache", "romfs:/Textures/Cache.png"))); - s_UserVector.push_back(std::make_pair(SystemID, Data::User(SystemID, "System", "romfs:/Textures/SystemSaves.png"))); - - 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 %u.", s_SaveDataSpaceOrder[i]); - continue; - } - - while (R_SUCCEEDED(fsSaveDataInfoReaderRead(&SaveInfoReader, &SaveInfo, 1, &TotalEntries)) && TotalEntries > 0) - { - // Skip this stuff - if (!Config::GetByKey(Config::Keys::ListAccountSystemSaves) && SaveInfo.save_data_type == FsSaveDataType_System && - SaveInfo.uid != 0) - { - continue; - } - - switch (SaveInfo.save_data_type) - { - case FsSaveDataType_Bcat: - { - SaveInfo.uid = {FsSaveDataType_Bcat}; - } - break; - - case FsSaveDataType_Device: - { - SaveInfo.uid = {FsSaveDataType_Device}; - } - break; - - case FsSaveDataType_Cache: - { - SaveInfo.uid = {FsSaveDataType_Cache}; - } - break; - - default: - break; - } - - // Test if save is even mountable. - if (Config::GetByKey(Config::Keys::OnlyListMountable) && !FS::MountSaveData(SaveInfo, FS::DEFAULT_SAVE_MOUNT)) - { - continue; - } - FsLib::CloseFileSystem(FS::DEFAULT_SAVE_MOUNT); - - // Find the user with info ID - 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.AddData(SaveInfo, PlayStats); - } - } - - for (auto &[AccountID, CurrentUser] : s_UserVector) - { - CurrentUser.SortData(); - } - - return true; -} - -void Data::GetUsers(std::vector &VectorOut) -{ - VectorOut.clear(); - for (auto &[AccountID, UserData] : s_UserVector) - { - VectorOut.push_back(&UserData); - } -} - -Data::TitleInfo *Data::GetTitleInfoByID(uint64_t ApplicationID) -{ - if (s_TitleInfoMap.find(ApplicationID) == s_TitleInfoMap.end()) - { - return nullptr; - } - return &s_TitleInfoMap.at(ApplicationID); -} - -void Data::GetTitleInfoByType(FsSaveDataType SaveType, std::vector &TitleInfoOut) -{ - // Clear vector JIC - TitleInfoOut.clear(); - - // Loop and push pointers - for (auto &[TitleID, Info] : s_TitleInfoMap) - { - if (Info.HasSaveDataType(SaveType)) - { - TitleInfoOut.push_back(&Info); - } - } -} diff --git a/Source/Data/TitleInfo.cpp b/Source/Data/TitleInfo.cpp deleted file mode 100644 index 4ccd087..0000000 --- a/Source/Data/TitleInfo.cpp +++ /dev/null @@ -1,304 +0,0 @@ -#include "Data/TitleInfo.hpp" -#include "Colors.hpp" -#include "Logger.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; - // 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)); - - // Sprintf title ids to language entries for safety. - sprintf(m_NACP.lang[SetLanguage_ENUS].name, "%016lX", ApplicationID); - sprintf(m_PathSafeTitle, "%016lX", ApplicationID); - - // Create a place holder icon. - int TextX = 128 - (SDL::Text::GetWidth(48, ApplicationIDHex.c_str()) / 2); - m_Icon = SDL::TextureManager::CreateLoadTexture(ApplicationIDHex, 256, 256, SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET); - m_Icon->Clear(Colors::DialogBox); - SDL::Text::Render(m_Icon->Get(), TextX, 104, 48, 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)); - // Get a path safe version of the title. - if (!StringUtil::SanitizeStringForPath(LanguageEntry->name, m_PathSafeTitle, 0x200)) - { - std::sprintf(m_PathSafeTitle, "%016lX", ApplicationID); - } - // 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::GetPathSafeTitle(void) -{ - return m_PathSafeTitle; -} - -const char *Data::TitleInfo::GetPublisher(void) -{ - NacpLanguageEntry *Entry = nullptr; - if (R_FAILED(nacpGetLanguageEntry(&m_NACP, &Entry))) - { - return nullptr; - } - return Entry->author; -} - -uint64_t Data::TitleInfo::GetSaveDataOwnerID(void) const -{ - return m_NACP.save_data_owner_id; -} - -uint64_t Data::TitleInfo::GetSaveDataSize(FsSaveDataType SaveType) const -{ - switch (SaveType) - { - case FsSaveDataType_Account: - { - return m_NACP.user_account_save_data_size; - } - break; - - case FsSaveDataType_Bcat: - { - return m_NACP.bcat_delivery_cache_storage_size; - } - break; - - case FsSaveDataType_Device: - { - return m_NACP.device_save_data_size; - } - break; - - case FsSaveDataType_Temporary: - { - return m_NACP.temporary_storage_size; - } - break; - - case FsSaveDataType_Cache: - { - return m_NACP.cache_storage_size; - } - break; - - default: - { - return 0; - } - break; - } - return 0; -} - -uint64_t Data::TitleInfo::GetSaveDataSizeMax(FsSaveDataType SaveType) const -{ - switch (SaveType) - { - case FsSaveDataType_Account: - { - return m_NACP.user_account_save_data_size_max > m_NACP.user_account_save_data_size ? m_NACP.user_account_save_data_size_max - : m_NACP.user_account_save_data_size; - } - break; - - case FsSaveDataType_Bcat: - { - return m_NACP.bcat_delivery_cache_storage_size; - } - break; - - case FsSaveDataType_Device: - { - return m_NACP.device_save_data_size_max > m_NACP.device_save_data_size ? m_NACP.device_save_data_size_max - : m_NACP.device_save_data_size; - } - break; - - case FsSaveDataType_Temporary: - { - return m_NACP.temporary_storage_size; - } - break; - - case FsSaveDataType_Cache: - { - return m_NACP.cache_storage_data_and_journal_size_max > m_NACP.cache_storage_size ? m_NACP.cache_storage_data_and_journal_size_max - : m_NACP.cache_storage_size; - } - break; - - default: - { - return 0; - } - break; - } - return 0; -} - -uint64_t Data::TitleInfo::GetJournalSize(FsSaveDataType SaveType) const -{ - switch (SaveType) - { - case FsSaveDataType_Account: - { - return m_NACP.user_account_save_data_journal_size; - } - break; - - case FsSaveDataType_Bcat: - { - // I'm just assuming this is right... - return m_NACP.bcat_delivery_cache_storage_size; - } - break; - - case FsSaveDataType_Device: - { - return m_NACP.device_save_data_journal_size; - } - break; - - case FsSaveDataType_Temporary: - { - // Again, just assuming. - return m_NACP.temporary_storage_size; - } - break; - - case FsSaveDataType_Cache: - { - return m_NACP.cache_storage_journal_size; - } - break; - - default: - { - return 0; - } - break; - } - return 0; -} - -uint64_t Data::TitleInfo::GetJournalSizeMax(FsSaveDataType SaveType) const -{ - switch (SaveType) - { - case FsSaveDataType_Account: - { - return m_NACP.user_account_save_data_journal_size_max > m_NACP.user_account_save_data_journal_size - ? m_NACP.user_account_save_data_journal_size_max - : m_NACP.user_account_save_data_journal_size; - } - break; - - case FsSaveDataType_Bcat: - { - return m_NACP.bcat_delivery_cache_storage_size; - } - break; - - case FsSaveDataType_Device: - { - return m_NACP.device_save_data_journal_size_max > m_NACP.device_save_data_journal_size ? m_NACP.device_save_data_journal_size_max - : m_NACP.device_save_data_journal_size; - } - break; - - case FsSaveDataType_Temporary: - { - return m_NACP.temporary_storage_size; - } - break; - - case FsSaveDataType_Cache: - { - return m_NACP.cache_storage_data_and_journal_size_max > m_NACP.cache_storage_journal_size - ? m_NACP.cache_storage_data_and_journal_size_max - : m_NACP.cache_storage_journal_size; - } - break; - - default: - { - return 0; - } - break; - } - return 0; -} - -bool Data::TitleInfo::HasSaveDataType(FsSaveDataType SaveType) -{ - switch (SaveType) - { - case FsSaveDataType_Account: - { - return m_NACP.user_account_save_data_size > 0 || m_NACP.user_account_save_data_size_max > 0; - } - break; - - case FsSaveDataType_Bcat: - { - return m_NACP.bcat_delivery_cache_storage_size > 0; - } - break; - - case FsSaveDataType_Device: - { - return m_NACP.device_save_data_size > 0 || m_NACP.device_save_data_size_max > 0; - } - break; - - case FsSaveDataType_Cache: - { - return m_NACP.cache_storage_size > 0 || m_NACP.cache_storage_data_and_journal_size_max > 0; - } - break; - - default: - { - return false; - } - break; - } - return false; -} - -SDL::SharedTexture Data::TitleInfo::GetIcon(void) const -{ - return m_Icon; -} diff --git a/Source/Data/User.cpp b/Source/Data/User.cpp deleted file mode 100644 index 8421b9d..0000000 --- a/Source/Data/User.cpp +++ /dev/null @@ -1,258 +0,0 @@ -#include "Data/User.hpp" -#include "Colors.hpp" -#include "Config.hpp" -#include "Data/Data.hpp" -#include "Logger.hpp" -#include "SDL.hpp" -#include "StringUtil.hpp" -#include -#include - -namespace -{ - constexpr int ICON_FONT_SIZE = 50; -} - -// Function used to sort user data. -static bool SortUserData(const Data::UserDataEntry &EntryA, const Data::UserDataEntry &EntryB) -{ - auto &[AppIDA, DataA] = EntryA; - auto &[AppIDB, DataB] = EntryB; - auto &[SaveInfoA, PlayStatsA] = DataA; - auto &[SaveInfoB, PlayStatsB] = DataB; - - // Favorites over all. - if (Config::IsFavorite(AppIDA) != Config::IsFavorite(AppIDB)) - { - return Config::IsFavorite(AppIDA); - } - - Data::TitleInfo *TitleInfoA = Data::GetTitleInfoByID(AppIDA); - Data::TitleInfo *TitleInfoB = Data::GetTitleInfoByID(AppIDB); - switch (Config::GetByKey(Config::Keys::TitleSortType)) - { - // Alpha - case 0: - { - // Get titles - const char *TitleA = TitleInfoA->GetTitle(); - const char *TitleB = TitleInfoB->GetTitle(); - - // Get the shortest of the two. - size_t TitleALength = std::char_traits::length(TitleA); - size_t TitleBLength = std::char_traits::length(TitleB); - size_t ShortestTitle = TitleALength < TitleBLength ? TitleALength : TitleBLength; - // Loop and compare codepoints. - for (size_t i = 0, j = 0; i < ShortestTitle;) - { - // Decode UTF-8 - uint32_t CodepointA = 0; - uint32_t CodepointB = 0; - ssize_t UnitCountA = decode_utf8(&CodepointA, reinterpret_cast(&TitleA[i])); - ssize_t UnitCountB = decode_utf8(&CodepointB, reinterpret_cast(&TitleB[j])); - - // Lower so case doesn't screw with it. - int CharA = std::tolower(CodepointA); - int CharB = std::tolower(CodepointB); - if (CharA != CharB) - { - return CharA < CharB; - } - - i += UnitCountA; - j += UnitCountB; - } - } - break; - - // Most played. - case 1: - { - return PlayStatsA.playtime > PlayStatsB.playtime; - } - break; - - // Last played. - case 2: - { - return PlayStatsA.last_timestamp_user > PlayStatsB.last_timestamp_user; - } - break; - } - return false; -} - -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 PathSafeNickname, std::string_view IconPath) - : m_AccountID(AccountID), m_Icon(SDL::TextureManager::CreateLoadTexture(PathSafeNickname, IconPath.data())) -{ - std::memcpy(m_PathSafeNickname, PathSafeNickname.data(), PathSafeNickname.length()); -} - -void Data::User::AddData(const FsSaveDataInfo &SaveInfo, const PdmPlayStatistics &PlayStats) -{ - uint64_t ApplicationID = SaveInfo.application_id == 0 ? SaveInfo.system_save_data_id : SaveInfo.application_id; - - m_UserData.push_back(std::make_pair(ApplicationID, std::make_pair(SaveInfo, PlayStats))); -} - -void Data::User::SortData(void) -{ - std::sort(m_UserData.begin(), m_UserData.end(), SortUserData); -} - -AccountUid Data::User::GetAccountID(void) const -{ - return m_AccountID; -} - -const char *Data::User::GetNickname(void) const -{ - return m_Nickname; -} - -const char *Data::User::GetPathSafeNickname(void) const -{ - return m_PathSafeNickname; -} - -size_t Data::User::GetTotalDataEntries(void) const -{ - return m_UserData.size(); -} - -uint64_t Data::User::GetApplicationIDAt(int Index) const -{ - if (Index < 0 || Index >= static_cast(m_UserData.size())) - { - return 0; - } - return m_UserData.at(Index).first; -} - -FsSaveDataInfo *Data::User::GetSaveInfoAt(int Index) -{ - if (Index < 0 || Index >= static_cast(m_UserData.size())) - { - return nullptr; - } - return &m_UserData.at(Index).second.first; -} - -PdmPlayStatistics *Data::User::GetPlayStatsAt(int Index) -{ - if (Index < 0 || Index >= static_cast(m_UserData.size())) - { - return nullptr; - } - return &m_UserData.at(Index).second.second; -} - -FsSaveDataInfo *Data::User::GetSaveInfoByID(uint64_t ApplicationID) -{ - auto FindTitle = std::find_if(m_UserData.begin(), m_UserData.end(), [ApplicationID](Data::UserDataEntry &Entry) { - return Entry.first == ApplicationID; - }); - - if (FindTitle == m_UserData.end()) - { - return nullptr; - } - return &FindTitle->second.first; -} - -PdmPlayStatistics *Data::User::GetPlayStatsByID(uint64_t ApplicationID) -{ - auto FindTitle = std::find_if(m_UserData.begin(), m_UserData.end(), [ApplicationID](Data::UserDataEntry &Entry) { - return Entry.first == ApplicationID; - }); - - if (FindTitle == m_UserData.end()) - { - return nullptr; - } - return &FindTitle->second.second; -} - -SDL_Texture *Data::User::GetIcon(void) -{ - return m_Icon->Get(); -} - -SDL::SharedTexture Data::User::GetSharedIcon(void) -{ - return m_Icon; -} - -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("Acc_%08X", m_AccountID.uid[0] & 0xFFFFFFFF); - - // Create icon - int TextX = 128 - (SDL::Text::GetWidth(ICON_FONT_SIZE, 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, - 128 - (ICON_FONT_SIZE / 2), - ICON_FONT_SIZE, - 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/FS/FileIO.cpp b/Source/FS/FileIO.cpp deleted file mode 100644 index 63a86c7..0000000 --- a/Source/FS/FileIO.cpp +++ /dev/null @@ -1,168 +0,0 @@ -#include "FS/FileIO.hpp" -#include "Logger.hpp" -#include "Strings.hpp" -#include -#include -#include -#include - -namespace -{ - // Size of buffer shared between threads. - // constexpr size_t FILE_BUFFER_SIZE = 0x600000; - // This one is just for testing something. - constexpr size_t FILE_BUFFER_SIZE = 0x80000; -} // namespace - -// Struct threads shared to read and write files. -typedef struct -{ - // Mutex to lock buffer. - std::mutex BufferLock; - // Conditional to wait on signals. - std::condition_variable BufferCondition; - // Bool to control signals. - bool BufferIsFull = false; - // Number of bytes read. - size_t ReadSize = 0; - // Shared (read) buffer. - std::unique_ptr ReadBuffer; -} FileTransferStruct; - -// This function Reads into the buffer. The other thread writes. -static void ReadThreadFunction(FsLib::File &SourceFile, std::shared_ptr SharedData) -{ - int64_t FileSize = SourceFile.GetSize(); - for (int64_t ReadCount = 0; ReadCount < FileSize;) - { - // Read data to shared buffer. - SharedData->ReadSize = SourceFile.Read(SharedData->ReadBuffer.get(), FILE_BUFFER_SIZE); - // Update local read count - ReadCount += SharedData->ReadSize; - // Signal to other thread buffer is full. - SharedData->BufferIsFull = true; - SharedData->BufferCondition.notify_one(); - // Wait for other thread to signal buffer is empty. Lock is released immediately, but it works and that's what matters. - std::unique_lock BufferLock(SharedData->BufferLock); - SharedData->BufferCondition.wait(BufferLock, [&SharedData]() { return SharedData->BufferIsFull == false; }); - } -} - -void FS::CopyFile(const FsLib::Path &Source, - const FsLib::Path &Destination, - uint64_t JournalSize, - std::string_view CommitDevice, - System::ProgressTask *Task) -{ - FsLib::File SourceFile(Source, FsOpenMode_Read); - FsLib::File DestinationFile(Destination, FsOpenMode_Create | FsOpenMode_Write, SourceFile.GetSize()); - if (!SourceFile.IsOpen() || !DestinationFile.IsOpen()) - { - Logger::Log("Error opening one of the files: %s", FsLib::GetErrorString()); - return; - } - - // Set status if task pointer was passed. - if (Task) - { - Task->SetStatus(Strings::GetByName(Strings::Names::CopyingFiles, 0), Source.CString()); - } - - // Shared struct both threads use - std::shared_ptr SharedData(new FileTransferStruct); - SharedData->ReadBuffer = std::make_unique(FILE_BUFFER_SIZE); - - // To do: Static thread or pool to avoid reallocating thread. - std::thread ReadThread(ReadThreadFunction, std::ref(SourceFile), SharedData); - - // This thread has a local buffer so the read thread can continue while this one writes. - std::unique_ptr LocalBuffer(new unsigned char[FILE_BUFFER_SIZE]); - - // Get file size for loop and set goal. - int64_t FileSize = SourceFile.GetSize(); - if (Task) - { - Task->Reset(static_cast(FileSize)); - } - - for (int64_t WriteCount = 0, ReadCount = 0, JournalCount = 0; WriteCount < FileSize;) - { - { - // Wait for lock/signal. - std::unique_lock BufferLock(SharedData->BufferLock); - SharedData->BufferCondition.wait(BufferLock, [&SharedData]() { return SharedData->BufferIsFull; }); - - // Record read count. - ReadCount = SharedData->ReadSize; - - // Copy shared to local. - std::memcpy(LocalBuffer.get(), SharedData->ReadBuffer.get(), ReadCount); - - // Signal buffer was copied and release mutex. - SharedData->BufferIsFull = false; - SharedData->BufferCondition.notify_one(); - } - - // Journaling size check. Breathing room is given. - if (JournalSize != 0 && (JournalCount + ReadCount) >= static_cast(JournalSize) - 0x100000) - { - // Reset journal count. - JournalCount = 0; - // Close Destination file, commit. - DestinationFile.Close(); - FsLib::CommitDataToFileSystem(CommitDevice); - // Reopen and seek to previous position since we created it with a size earlier. - DestinationFile.Open(Destination, FsOpenMode_Write); - DestinationFile.Seek(WriteCount, DestinationFile.Beginning); - } - // Write to destination - DestinationFile.Write(LocalBuffer.get(), ReadCount); - // Update write and journal count. - WriteCount += ReadCount; - JournalCount += ReadCount; - // Update task if passed. - if (Task) - { - Task->UpdateCurrent(static_cast(WriteCount)); - } - } - // Wait for read thread and free it. - ReadThread.join(); -} - -void FS::CopyDirectory(const FsLib::Path &Source, - const FsLib::Path &Destination, - uint64_t JournalSize, - std::string_view CommitDevice, - System::ProgressTask *Task) -{ - FsLib::Directory SourceDir(Source); - if (!SourceDir.IsOpen()) - { - Logger::Log("Error opening directory for reading: %s", FsLib::GetErrorString()); - return; - } - - for (int64_t i = 0; i < SourceDir.GetEntryCount(); i++) - { - if (SourceDir.EntryAtIsDirectory(i)) - { - FsLib::Path NewSource = Source / SourceDir[i]; - FsLib::Path NewDestination = Destination / SourceDir[i]; - // Try to create new destination folder and continue loop on failure. - if (!FsLib::DirectoryExists(NewDestination) && !FsLib::CreateDirectory(NewDestination)) - { - Logger::Log("Error creating new destination directory: %s", FsLib::GetErrorString()); - continue; - } - - FS::CopyDirectory(NewSource, NewDestination, JournalSize, CommitDevice, Task); - } - else - { - FsLib::Path FullSource = Source / SourceDir[i]; - FsLib::Path FullDestination = Destination / SourceDir[i]; - FS::CopyFile(FullSource, FullDestination, JournalSize, CommitDevice, Task); - } - } -} diff --git a/Source/FS/SaveMount.cpp b/Source/FS/SaveMount.cpp deleted file mode 100644 index 2b0a3b8..0000000 --- a/Source/FS/SaveMount.cpp +++ /dev/null @@ -1,64 +0,0 @@ -#include "FS/SaveMount.hpp" -#include "FsLib.hpp" - -bool FS::MountSaveData(const FsSaveDataInfo &SaveInfo, std::string_view DeviceName) -{ - switch (SaveInfo.save_data_type) - { - case FsSaveDataType_System: - { - // This should work because JKSV's internal ID for the system user is 0. - return FsLib::OpenSystemSaveFileSystem(DeviceName, - SaveInfo.system_save_data_id, - static_cast(SaveInfo.save_data_space_id), - static_cast(SaveInfo.save_data_rank), - SaveInfo.uid); - } - break; - - case FsSaveDataType_Account: - { - return FsLib::OpenAccountSaveFileSystem(DeviceName, - SaveInfo.application_id, - SaveInfo.uid, - static_cast(SaveInfo.save_data_space_id), - static_cast(SaveInfo.save_data_rank)); - } - break; - - case FsSaveDataType_Bcat: - { - return FsLib::OpenBCATSaveFileSystem(DeviceName, SaveInfo.application_id); - } - break; - - case FsSaveDataType_Device: - { - return FsLib::OpenDeviceSaveFileSystem(DeviceName, SaveInfo.application_id); - } - break; - - case FsSaveDataType_Temporary: - { - return FsLib::OpenTemporarySaveFileSystem(DeviceName); - } - break; - - case FsSaveDataType_Cache: - { - return FsLib::OpenCacheSaveFileSystem(DeviceName, - SaveInfo.application_id, - SaveInfo.save_data_index, - static_cast(SaveInfo.save_data_space_id), - static_cast(SaveInfo.save_data_rank)); - } - break; - - case FsSaveDataType_SystemBcat: - { - return FsLib::OpenSystemBCATSaveFileSystem(DeviceName, SaveInfo.system_save_data_id); - } - break; - } - return false; -} diff --git a/Source/FS/ZipIO.cpp b/Source/FS/ZipIO.cpp deleted file mode 100644 index ec0710a..0000000 --- a/Source/FS/ZipIO.cpp +++ /dev/null @@ -1,295 +0,0 @@ -#include "FS/ZipIO.hpp" -#include "Config.hpp" -#include "Logger.hpp" -#include "Strings.hpp" -#include -#include -#include -#include -#include -#include - -namespace -{ - // Size used for Zipping files. - constexpr size_t ZIP_BUFFER_SIZE = 0x80000; - // Size used for unzipping. - constexpr size_t UNZIP_BUFFER_SIZE = 0x600000; -} // namespace - -// Shared struct for Zip/File IO -typedef struct -{ - // Mutex and condition for buffer. - std::mutex BufferLock; - std::condition_variable BufferCondition; - bool BufferIsFull = false; - // Number of bytes read from file. - ssize_t ReadCount = 0; - // Shared/reading buffer. - std::unique_ptr SharedBuffer; -} ZipIOStruct; - -// Function for reading files for Zipping. -static void ZipReadThreadFunction(FsLib::File &Source, std::shared_ptr SharedData) -{ - int64_t FileSize = Source.GetSize(); - for (int64_t ReadCount = 0; ReadCount < FileSize;) - { - // Read into shared buffer. - SharedData->ReadCount = Source.Read(SharedData->SharedBuffer.get(), ZIP_BUFFER_SIZE); - // Update read count - ReadCount += SharedData->ReadCount; - // Signal other thread buffer is ready to go. - SharedData->BufferIsFull = true; - SharedData->BufferCondition.notify_one(); - // Wait for other thread to release lock on buffer so this thread can read again. - std::unique_lock BufferLock(SharedData->BufferLock); - SharedData->BufferCondition.wait(BufferLock, [&SharedData]() { return SharedData->BufferIsFull == false; }); - } -} - -// Function for reading data from Zip to buffer. -static void UnzipReadThreadFunction(unzFile Source, int64_t FileSize, std::shared_ptr SharedData) -{ - for (int64_t ReadCount = 0; ReadCount < FileSize;) - { - // Read from zip file. - SharedData->ReadCount = unzReadCurrentFile(Source, SharedData->SharedBuffer.get(), UNZIP_BUFFER_SIZE); - - ReadCount += SharedData->ReadCount; - - SharedData->BufferIsFull = true; - SharedData->BufferCondition.notify_one(); - - std::unique_lock BufferLock(SharedData->BufferLock); - SharedData->BufferCondition.wait(BufferLock, [&SharedData]() { return SharedData->BufferIsFull == false; }); - } -} - -void FS::CopyDirectoryToZip(const FsLib::Path &Source, zipFile Destination, System::ProgressTask *Task) -{ - FsLib::Directory SourceDir(Source); - if (!SourceDir.IsOpen()) - { - Logger::Log("Error opening source directory: %s", FsLib::GetErrorString()); - return; - } - - for (int64_t i = 0; i < SourceDir.GetEntryCount(); i++) - { - if (SourceDir.EntryAtIsDirectory(i)) - { - FsLib::Path NewSource = Source / SourceDir[i]; - FS::CopyDirectoryToZip(NewSource, Destination, Task); - } - else - { - // Open source file. - FsLib::Path FullSource = Source / SourceDir[i]; - FsLib::File SourceFile(FullSource, FsOpenMode_Read); - if (!SourceFile.IsOpen()) - { - Logger::Log("Error zipping file: %s", FsLib::GetErrorString()); - continue; - } - - // Date for file(s) - std::time_t Timer; - std::time(&Timer); - std::tm *LocalTime = std::localtime(&Timer); - zip_fileinfo FileInfo = {.tmz_date = {.tm_sec = LocalTime->tm_sec, - .tm_min = LocalTime->tm_min, - .tm_hour = LocalTime->tm_hour, - .tm_mday = LocalTime->tm_mday, - .tm_mon = LocalTime->tm_mon, - .tm_year = LocalTime->tm_year + 1900}, - .dosDate = 0, - .internal_fa = 0, - .external_fa = 0}; - - // Create new file in zip - const char *FileNameBegin = std::strchr(FullSource.GetPath(), '/') + 1; - int ZipError = zipOpenNewFileInZip64(Destination, - FileNameBegin, - &FileInfo, - NULL, - 0, - NULL, - 0, - NULL, - Z_DEFLATED, - Config::GetByKey(Config::Keys::ZipCompressionLevel), - 1); - if (ZipError != ZIP_OK) - { - Logger::Log("Error creating file in zip: %i.", ZipError); - continue; - } - - // Shared data for thread. - std::shared_ptr SharedData(new ZipIOStruct); - SharedData->SharedBuffer = std::make_unique(ZIP_BUFFER_SIZE); - - // Local buffer for writing. - std::unique_ptr LocalBuffer(new unsigned char[ZIP_BUFFER_SIZE]); - - // Update task if passed. - if (Task) - { - Task->SetStatus(Strings::GetByName(Strings::Names::CopyingFiles, 1), FullSource.CString()); - Task->Reset(static_cast(SourceFile.GetSize())); - } - - std::thread ReadThread(ZipReadThreadFunction, std::ref(SourceFile), SharedData); - - int64_t FileSize = SourceFile.GetSize(); - for (int64_t WriteCount = 0, ReadCount = 0; WriteCount < FileSize;) - { - { - // Wait for buffer signal - std::unique_lock BufferLock(SharedData->BufferLock); - SharedData->BufferCondition.wait(BufferLock, [&SharedData]() { return SharedData->BufferIsFull; }); - - // Save read count, copy shared to local. - ReadCount = SharedData->ReadCount; - std::memcpy(LocalBuffer.get(), SharedData->SharedBuffer.get(), ReadCount); - - // Signal copy was good and release lock. - SharedData->BufferIsFull = false; - SharedData->BufferCondition.notify_one(); - } - // Write - ZipError = zipWriteInFileInZip(Destination, LocalBuffer.get(), ReadCount); - if (ZipError != ZIP_OK) - { - Logger::Log("Error writing data to zip: %i.", ZipError); - } - // Update count and status - WriteCount += ReadCount; - if (Task) - { - Task->UpdateCurrent(static_cast(WriteCount)); - } - } - // Wait for thread - ReadThread.join(); - // Close file in zip - zipCloseFileInZip(Destination); - } - } -} - -void FS::CopyZipToDirectory(unzFile Source, - const FsLib::Path &Destination, - uint64_t JournalSize, - std::string_view CommitDevice, - System::ProgressTask *Task) -{ - int ZipError = unzGoToFirstFile(Source); - if (ZipError != UNZ_OK) - { - Logger::Log("Error opening empty ZIP file: %i.", ZipError); - return; - } - - do - { - // Get file information. - unz_file_info64 CurrentFileInfo; - char FileName[FS_MAX_PATH] = {0}; - if (unzGetCurrentFileInfo64(Source, &CurrentFileInfo, FileName, FS_MAX_PATH, NULL, 0, NULL, 0) != UNZ_OK || - unzOpenCurrentFile(Source) != UNZ_OK) - { - Logger::Log("Error opening and getting information for file in zip."); - continue; - } - - // Create full path to item, make sure directories are created if needed. - FsLib::Path FullDestination = Destination / FileName; - - FsLib::Path Directories = FullDestination.SubPath(FullDestination.FindLastOf('/') - 1); - // To do: Make FsLib handle this correctly. First condition is a workaround for now... - if (Directories.IsValid() && !FsLib::CreateDirectoriesRecursively(Directories)) - { - Logger::Log("Error creating zip file path \"%s\": %s", Directories.CString(), FsLib::GetErrorString()); - continue; - } - - FsLib::File DestinationFile(FullDestination, FsOpenMode_Create | FsOpenMode_Write, CurrentFileInfo.uncompressed_size); - if (!DestinationFile.IsOpen()) - { - Logger::Log("Error creating file from zip: %s", FsLib::GetErrorString()); - continue; - } - - // Shared data for both threads - std::shared_ptr SharedData(new ZipIOStruct); - SharedData->SharedBuffer = std::make_unique(UNZIP_BUFFER_SIZE); - - // Spawn read thread. - std::thread ReadThread(UnzipReadThreadFunction, Source, CurrentFileInfo.uncompressed_size, SharedData); - - // Local buffer - std::unique_ptr LocalBuffer(new unsigned char[UNZIP_BUFFER_SIZE]); - - // Set status - if (Task) - { - Task->SetStatus(Strings::GetByName(Strings::Names::CopyingFiles, 3), FileName); - Task->Reset(static_cast(CurrentFileInfo.uncompressed_size)); - } - - for (int64_t WriteCount = 0, ReadCount = 0, JournalCount = 0; WriteCount < static_cast(CurrentFileInfo.uncompressed_size);) - { - { - // Wait for buffer. - std::unique_lock BufferLock(SharedData->BufferLock); - SharedData->BufferCondition.wait(BufferLock, [&SharedData]() { return SharedData->BufferIsFull; }); - - // Save read count for later - ReadCount = SharedData->ReadCount; - - // Copy shared to local - std::memcpy(LocalBuffer.get(), SharedData->SharedBuffer.get(), ReadCount); - - // Signal this thread is done. - SharedData->BufferIsFull = false; - SharedData->BufferCondition.notify_one(); - } - - // Journaling check - if (JournalCount + ReadCount >= static_cast(JournalSize)) - { - // Close. - DestinationFile.Close(); - // Commit - if (!FsLib::CommitDataToFileSystem(CommitDevice)) - { - Logger::Log("Error committing data to save: %s", FsLib::GetErrorString()); - } - // Reopen, seek to previous position. - DestinationFile.Open(FullDestination, FsOpenMode_Write); - DestinationFile.Seek(WriteCount, DestinationFile.Beginning); - // Reset journal - JournalCount = 0; - } - // Write data. - DestinationFile.Write(LocalBuffer.get(), ReadCount); - // Update write and journal count - WriteCount += ReadCount; - JournalCount += JournalCount; - // Update status - if (Task) - { - Task->UpdateCurrent(WriteCount); - } - } - // Close file and commit again just for good measure. - DestinationFile.Close(); - if (!FsLib::CommitDataToFileSystem(CommitDevice)) - { - Logger::Log("Error performing final file commit: %s", FsLib::GetErrorString()); - } - } while (unzGoToNextFile(Source) != UNZ_END_OF_LIST_OF_FILE); -} diff --git a/Source/Input.cpp b/Source/Input.cpp deleted file mode 100644 index 425e417..0000000 --- a/Source/Input.cpp +++ /dev/null @@ -1,32 +0,0 @@ -#include "Input.hpp" - -namespace -{ - PadState s_Gamepad; -} - -void Input::Initialize(void) -{ - padConfigureInput(1, HidNpadStyleSet_NpadStandard); - padInitializeDefault(&s_Gamepad); -} - -void Input::Update(void) -{ - padUpdate(&s_Gamepad); -} - -bool Input::ButtonPressed(HidNpadButton Button) -{ - return (s_Gamepad.buttons_cur & Button) && !(s_Gamepad.buttons_old & Button); -} - -bool Input::ButtonHeld(HidNpadButton Button) -{ - return (s_Gamepad.buttons_cur & Button) && (s_Gamepad.buttons_old & Button); -} - -bool Input::ButtonReleased(HidNpadButton Button) -{ - return (s_Gamepad.buttons_old & Button) && !(s_Gamepad.buttons_cur & Button); -} diff --git a/Source/JKSV.cpp b/Source/JKSV.cpp deleted file mode 100644 index a585442..0000000 --- a/Source/JKSV.cpp +++ /dev/null @@ -1,191 +0,0 @@ -#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" -#include "SDL.hpp" -#include "Strings.hpp" -#include - -#define ABORT_ON_FAILURE(x) \ - if (!x) \ - { \ - return; \ - } - -namespace -{ - constexpr uint8_t BUILD_MON = 1; - constexpr uint8_t BUILD_DAY = 6; - constexpr uint16_t BUILD_YEAR = 2025; -} // namespace - -template -static bool InitializeService(Result (*Function)(Args...), const char *ServiceName, Args... Arguments) -{ - Result Error = (*Function)(Arguments...); - if (R_FAILED(Error)) - { - Logger::Log("Error initializing %s: 0x%X.", Error); - return false; - } - return true; -} - -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()); - - // Services. - // Using administrator so JKSV can still run in Applet mode. - ABORT_ON_FAILURE(InitializeService(accountInitialize, "Account", AccountServiceType_Administrator)); - ABORT_ON_FAILURE(InitializeService(nsInitialize, "NS")); - ABORT_ON_FAILURE(InitializeService(pdmqryInitialize, "PDMQry")); - ABORT_ON_FAILURE(InitializeService(plInitialize, "PL", PlServiceType_User)); - ABORT_ON_FAILURE(InitializeService(pmshellInitialize, "PMShell")); - ABORT_ON_FAILURE(InitializeService(setInitialize, "Set")); - ABORT_ON_FAILURE(InitializeService(setsysInitialize, "SetSys")); - ABORT_ON_FAILURE(InitializeService(socketInitializeDefault, "Socket")); - - // Input doesn't have anything to return. - Input::Initialize(); - - // Neither does config. - Config::Initialize(); - - // Get and create working directory. There isn't much of an FS anymore. - FsLib::Path WorkingDirectory = Config::GetWorkingDirectory(); - if (!FsLib::DirectoryExists(WorkingDirectory) && !FsLib::CreateDirectoriesRecursively(WorkingDirectory)) - { - Logger::Log("Error creating working directory: %s", FsLib::GetErrorString()); - return; - } - - // 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); - SDL::Text::AddColorCharacter(L'<', Colors::Yellow); - SDL::Text::AddColorCharacter(L'>', Colors::Green); - SDL::Text::AddColorCharacter(L'^', Colors::Pink); - - // This is to check whether the author wanted credit for their work. - m_ShowTranslationInfo = std::char_traits::compare(Strings::GetByName(Strings::Names::TranslationInfo, 1), "NULL", 4) != 0; - - // This can't be in an initializer list because it needs SDL initialized. - m_HeaderIcon = SDL::TextureManager::CreateLoadTexture("HeaderIcon", "romfs:/Textures/HeaderIcon.png"); - - // Push initial main menu state. - JKSV::PushState(std::make_shared()); - - m_IsRunning = true; -} - -JKSV::~JKSV() -{ - socketExit(); - setsysExit(); - setExit(); - pmshellExit(); - plExit(); - pdmqryExit(); - nsExit(); - accountExit(); - SDL::Text::Exit(); - SDL::Exit(); - FsLib::Exit(); -} - -bool JKSV::IsRunning(void) const -{ - return m_IsRunning; -} - -void JKSV::Update(void) -{ - Input::Update(); - - if (Input::ButtonPressed(HidNpadButton_Plus) && !m_StateVector.empty() && m_StateVector.back()->IsClosable()) - { - m_IsRunning = false; - } - - if (!m_StateVector.empty()) - { - while (!m_StateVector.back()->IsActive()) - { - m_StateVector.back()->TakeFocus(); - m_StateVector.pop_back(); - m_StateVector.back()->GiveFocus(); - } - m_StateVector.back()->Update(); - } -} - -void JKSV::Render(void) -{ - SDL::FrameBegin(Colors::ClearColor); - // Top and bottom divider lines. - SDL::RenderLine(NULL, 30, 88, 1250, 88, Colors::White); - SDL::RenderLine(NULL, 30, 648, 1250, 648, Colors::White); - // Icon - m_HeaderIcon->Render(NULL, 66, 27); - // "JKSV" - SDL::Text::Render(NULL, 130, 32, 34, SDL::Text::NO_TEXT_WRAP, Colors::White, "JKSV"); - // Translation info in bottom left. - if (m_ShowTranslationInfo) - { - SDL::Text::Render(NULL, - 8, - 680, - 14, - SDL::Text::NO_TEXT_WRAP, - Colors::White, - Strings::GetByName(Strings::Names::TranslationInfo, 0), - Strings::GetByName(Strings::Names::TranslationInfo, 1)); - } - // Build date - SDL::Text::Render(NULL, 8, 700, 14, SDL::Text::NO_TEXT_WRAP, Colors::White, "v. %02d.%02d.%04d", BUILD_MON, BUILD_DAY, BUILD_YEAR); - - // State render loop. - if (!m_StateVector.empty()) - { - for (auto &CurrentState : m_StateVector) - { - CurrentState->Render(); - } - } - - 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/Keyboard.cpp b/Source/Keyboard.cpp deleted file mode 100644 index e86099c..0000000 --- a/Source/Keyboard.cpp +++ /dev/null @@ -1,31 +0,0 @@ -#include "Keyboard.hpp" -#include - -bool Keyboard::GetInput(SwkbdType KeyboardType, std::string_view DefaultText, std::string_view Header, char *StringOut, size_t StringLength) -{ - // Setup keyboard. - SwkbdConfig Keyboard; - swkbdCreate(&Keyboard, 0); // Old JKSV actually used dictionary words, but I don't feel like implementing them again. - swkbdConfigSetBlurBackground(&Keyboard, true); - swkbdConfigSetInitialText(&Keyboard, DefaultText.data()); - swkbdConfigSetHeaderText(&Keyboard, Header.data()); - swkbdConfigSetGuideText(&Keyboard, Header.data()); - swkbdConfigSetType(&Keyboard, KeyboardType); - swkbdConfigSetStringLenMax(&Keyboard, StringLength); - swkbdConfigSetKeySetDisableBitmask(&Keyboard, SwkbdKeyDisableBitmask_ForwardSlash | SwkbdKeyDisableBitmask_Backslash); - - // If it fails, just return. - if (R_FAILED(swkbdShow(&Keyboard, StringOut, StringLength))) - { - return false; - } - - // If the string is empty, assume failure or cancel. - if (std::char_traits::length(StringOut) == 0) - { - return false; - } - - // I wish this was more like the 3DS keyboard cause that actually returned what button was pressed... - return true; -} diff --git a/Source/Logger.cpp b/Source/Logger.cpp deleted file mode 100644 index 60c7208..0000000 --- a/Source/Logger.cpp +++ /dev/null @@ -1,32 +0,0 @@ -#include "Logger.hpp" -#include "FsLib.hpp" -#include - -namespace -{ - // Path to log file. - FsLib::Path s_LogFilePath; - // Size of va buffer for log. - constexpr size_t VA_BUFFER_SIZE = 0x1000; -} // namespace - -void Logger::Initialize(void) -{ - // To do: Update this once config is implemented. - s_LogFilePath = "sdmc:/JKSV/JKSV.log"; - FsLib::File LogFile(s_LogFilePath, FsOpenMode_Create | FsOpenMode_Write); -} - -void Logger::Log(const char *Format, ...) -{ - char VaBuffer[VA_BUFFER_SIZE] = {0}; - - std::va_list VaList; - va_start(VaList, Format); - vsnprintf(VaBuffer, VA_BUFFER_SIZE, Format, VaList); - va_end(VaList); - - FsLib::File LogFile(s_LogFilePath, FsOpenMode_Append); - LogFile << VaBuffer << "\n"; - LogFile.Flush(); -} diff --git a/Source/Main.cpp b/Source/Main.cpp deleted file mode 100644 index 5af27f8..0000000 --- a/Source/Main.cpp +++ /dev/null @@ -1,15 +0,0 @@ -#include "Config.hpp" -#include "JKSV.hpp" -#include - -int main(void) -{ - JKSV Jksv{}; - while (appletMainLoop() && Jksv.IsRunning()) - { - Jksv.Update(); - Jksv.Render(); - } - Config::Save(); - return 0; -} diff --git a/Source/StringUtil.cpp b/Source/StringUtil.cpp deleted file mode 100644 index fcf13bf..0000000 --- a/Source/StringUtil.cpp +++ /dev/null @@ -1,108 +0,0 @@ -#include "StringUtil.hpp" -#include -#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, ...) -{ - char VaBuffer[VA_BUFFER_SIZE]; - - std::va_list VaList; - va_start(VaList, Format); - vsnprintf(VaBuffer, VA_BUFFER_SIZE, Format, VaList); - va_end(VaList); - - 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; -} - -std::string StringUtil::GetDateString(StringUtil::DateFormat Format) -{ - char StringBuffer[0x80]; - - std::time_t Timer; - std::time(&Timer); - std::tm *LocalTime = std::localtime(&Timer); - - switch (Format) - { - case StringUtil::DateFormat::YearMonthDay: - { - std::strftime(StringBuffer, 0x80, "%Y-%m-%d_%H-%M-%S", LocalTime); - } - break; - - case StringUtil::DateFormat::YearDayMonth: - { - std::strftime(StringBuffer, 0x80, "%Y-%d-%m_%H-%M-%S", LocalTime); - } - break; - } - - return std::string(StringBuffer); -} diff --git a/Source/Strings.cpp b/Source/Strings.cpp deleted file mode 100644 index 1d46e00..0000000 --- a/Source/Strings.cpp +++ /dev/null @@ -1,120 +0,0 @@ -#include "Strings.hpp" -#include "FsLib.hpp" -#include "JSON.hpp" -#include "StringUtil.hpp" -#include -#include -#include - -namespace -{ - // This is the actual map where the strings are. - std::map, std::string> s_StringMap; - // This map is for matching files to the language value - std::unordered_map s_FileMap = {{SetLanguage_JA, "JA.json"}, - {SetLanguage_ENUS, "ENUS.json"}, - {SetLanguage_FR, "FR.json"}, - {SetLanguage_DE, "DE.json"}, - {SetLanguage_IT, "IT.json"}, - {SetLanguage_ES, "ES.json"}, - {SetLanguage_ZHCN, "ZHCN.json"}, - {SetLanguage_KO, "KO.json"}, - {SetLanguage_NL, "NL.json"}, - {SetLanguage_PT, "PT.json"}, - {SetLanguage_RU, "RU.json"}, - {SetLanguage_ZHTW, "ZHTW.json"}, - {SetLanguage_ENGB, "ENGB.json"}, - {SetLanguage_FRCA, "FRCA.json"}, - {SetLanguage_ES419, "ES419.json"}, - {SetLanguage_ZHHANS, "ZHCN.json"}, - {SetLanguage_ZHHANT, "ZHTW.json"}, - {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"; - - uint64_t LanguageCode = 0; - Result SetError = setGetLanguageCode(&LanguageCode); - if (R_FAILED(SetError)) - { - return ReturnPath / s_FileMap.at(SetLanguage_ENUS); - } - - SetLanguage Language; - SetError = setMakeLanguage(LanguageCode, &Language); - if (R_FAILED(SetError)) - { - return ReturnPath / s_FileMap.at(SetLanguage_ENUS); - } - 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(); - - JSON::Object TextJSON = JSON::NewObject(json_object_from_file, StringsPath.CString()); - if (!TextJSON) - { - return false; - } - - json_object_iterator StringIterator = json_object_iter_begin(TextJSON.get()); - json_object_iterator StringEnd = json_object_iter_end(TextJSON.get()); - while (!json_object_iter_equal(&StringIterator, &StringEnd)) - { - // Get name of string(s) and pointer to array - const char *StringName = json_object_iter_peek_name(&StringIterator); - json_object *StringArray = json_object_iter_peek_value(&StringIterator); - - // Loop through array and add them to map so I can be lazier and not have to edit code or do shit to add more strings. - size_t ArrayLength = json_object_array_length(StringArray); - for (size_t i = 0; i < ArrayLength; i++) - { - json_object *String = json_object_array_get_idx(StringArray, i); - s_StringMap[std::make_pair(StringName, static_cast(i))] = json_object_get_string(String); - } - json_object_iter_next(&StringIterator); - } - - // Loop through entire map and replace the buttons. - for (auto &[Key, String] : s_StringMap) - { - ReplaceButtonsInString(String); - } - - return true; -} - -const char *Strings::GetByName(std::string_view Name, int Index) -{ - if (s_StringMap.find(std::make_pair(Name.data(), Index)) == s_StringMap.end()) - { - return nullptr; - } - return s_StringMap.at(std::make_pair(Name.data(), Index)).c_str(); -} diff --git a/Source/System/ProgressTask.cpp b/Source/System/ProgressTask.cpp deleted file mode 100644 index c884b96..0000000 --- a/Source/System/ProgressTask.cpp +++ /dev/null @@ -1,22 +0,0 @@ -#include "System/ProgressTask.hpp" - -void System::ProgressTask::Reset(double Goal) -{ - m_Current = 0; - m_Goal = Goal; -} - -void System::ProgressTask::UpdateCurrent(double Current) -{ - m_Current = Current; -} - -double System::ProgressTask::GetGoal(void) const -{ - return m_Goal; -} - -double System::ProgressTask::GetCurrentProgress(void) const -{ - return m_Current / m_Goal; -} diff --git a/Source/System/Task.cpp b/Source/System/Task.cpp deleted file mode 100644 index f9533c0..0000000 --- a/Source/System/Task.cpp +++ /dev/null @@ -1,41 +0,0 @@ -#include "System/Task.hpp" -#include - -namespace -{ - constexpr size_t VA_BUFFER_SIZE = 0x1000; -} - -System::Task::~Task() -{ - m_Thread.join(); -} - -bool System::Task::IsRunning(void) const -{ - return m_IsRunning; -} - -void System::Task::Finished(void) -{ - m_IsRunning = false; -} - -void System::Task::SetStatus(const char *Format, ...) -{ - char VaBuffer[VA_BUFFER_SIZE]; - - std::va_list VaList; - va_start(VaList, Format); - vsnprintf(VaBuffer, VA_BUFFER_SIZE, Format, VaList); - va_end(VaList); - - std::scoped_lock StatusLock(m_StatusLock); - m_Status = VaBuffer; -} - -std::string System::Task::GetStatus(void) -{ - std::scoped_lock StatusLock(m_StatusLock); - return m_Status; -} diff --git a/Source/System/Timer.cpp b/Source/System/Timer.cpp deleted file mode 100644 index ce0369a..0000000 --- a/Source/System/Timer.cpp +++ /dev/null @@ -1,23 +0,0 @@ -#include "System/Timer.hpp" - -System::Timer::Timer(uint64_t TriggerTicks) : m_StartingTicks(SDL_GetTicks64()), m_TriggerTicks(TriggerTicks) {}; - -bool System::Timer::IsTriggered(void) -{ - uint64_t CurrentTicks = SDL_GetTicks64(); - - // Nope - if (CurrentTicks - m_StartingTicks < m_TriggerTicks) - { - return false; - } - // Reset starting ticks. - m_StartingTicks = CurrentTicks; - // Trigger me timbers~ - return true; -} - -void System::Timer::Restart(void) -{ - m_StartingTicks = SDL_GetTicks64(); -} diff --git a/Source/UI/ColorMod.cpp b/Source/UI/ColorMod.cpp deleted file mode 100644 index c055910..0000000 --- a/Source/UI/ColorMod.cpp +++ /dev/null @@ -1,18 +0,0 @@ -#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/IconMenu.cpp b/Source/UI/IconMenu.cpp deleted file mode 100644 index 3988a40..0000000 --- a/Source/UI/IconMenu.cpp +++ /dev/null @@ -1,42 +0,0 @@ -#include "UI/IconMenu.hpp" -#include "Colors.hpp" -#include "UI/RenderFunctions.hpp" - -UI::IconMenu::IconMenu(int X, int Y, int RenderTargetHeight) : Menu(X, Y, 152, 80, RenderTargetHeight) {}; - -void UI::IconMenu::Update(bool HasFocus) -{ - Menu::Update(HasFocus); -} - -void UI::IconMenu::Render(SDL_Texture *Target, bool HasFocus) -{ - if (HasFocus) - { - m_ColorMod.Update(); - } - - for (int i = 0, TempY = m_Y; i < static_cast(m_Options.size()); i++, TempY += m_OptionHeight) - { - // Clear target. - m_OptionTarget->Clear(Colors::Transparent); - if (i == m_Selected) - { - if (HasFocus) - { - UI::RenderBoundingBox(Target, m_X - 8, TempY - 8, 152, 146, m_ColorMod); - } - SDL::RenderRectFill(m_OptionTarget->Get(), 0, 0, 4, 130, {0x00FFC5FF}); - } - //m_Options.at(i)->Render(m_OptionTarget->Get(), 0, 0); - m_Options.at(i)->RenderStretched(m_OptionTarget->Get(), 8, 1, 128, 128); - m_OptionTarget->Render(Target, m_X, TempY); - } -} - -void UI::IconMenu::AddOption(SDL::SharedTexture NewOption) -{ - // Parent needs a text option to work correctly. - Menu::AddOption("ICON"); - m_Options.push_back(NewOption); -} diff --git a/Source/UI/Menu.cpp b/Source/UI/Menu.cpp deleted file mode 100644 index 84b6c9a..0000000 --- a/Source/UI/Menu.cpp +++ /dev/null @@ -1,157 +0,0 @@ -#include "UI/Menu.hpp" -#include "Colors.hpp" -#include "Config.hpp" -#include "Input.hpp" -#include "UI/RenderFunctions.hpp" -#include - -UI::Menu::Menu(int X, int Y, int Width, int FontSize, int RenderTargetHeight) - : m_X(X), m_Y(Y), m_OptionHeight(std::ceil(static_cast(FontSize) * 1.8f)), m_OriginalY(Y), m_TargetY(Y), m_Width(Width), - m_FontSize(FontSize), m_RenderTargetHeight(RenderTargetHeight) -{ - // Create render target for options - static int MenuID = 0; - std::string MenuTargetName = "Menu_" + std::to_string(MenuID++); - m_OptionTarget = - SDL::TextureManager::CreateLoadTexture(MenuTargetName, m_Width, m_OptionHeight, SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET); - - // Calculate around how many options can be shown on the render target at once. - m_MaxDisplayOptions = (RenderTargetHeight - m_OriginalY) / m_OptionHeight; - m_ScrollLength = std::floor(static_cast(m_MaxDisplayOptions) / 2.0f); -} - -void UI::Menu::Update(bool HasFocus) -{ - // Bail if there's nothing to update. - if (m_Options.empty()) - { - return; - } - - int OptionsSize = m_Options.size(); - if (Input::ButtonPressed(HidNpadButton_AnyUp) && --m_Selected < 0) - { - m_Selected = OptionsSize - 1; - } - else if (Input::ButtonPressed(HidNpadButton_AnyDown) && ++m_Selected >= OptionsSize) - { - m_Selected = 0; - } - else if (Input::ButtonPressed(HidNpadButton_AnyLeft) && (m_Selected -= m_ScrollLength) < 0) - { - m_Selected = 0; - } - else if (Input::ButtonPressed(HidNpadButton_AnyRight) && (m_Selected += m_ScrollLength) >= OptionsSize) - { - m_Selected = OptionsSize - 1; - } - else if (Input::ButtonPressed(HidNpadButton_L) && (m_Selected -= m_ScrollLength * 3) < 0) - { - m_Selected = 0; - } - else if (Input::ButtonPressed(HidNpadButton_R) && (m_Selected += m_ScrollLength * 3) >= OptionsSize) - { - m_Selected = OptionsSize - 1; - } - - // Don't bother continuing further if there's no reason to scroll. - if (static_cast(m_Options.size()) <= m_MaxDisplayOptions) - { - return; - } - - if (m_Selected < m_ScrollLength) - { - m_TargetY = m_OriginalY; - } - else if (m_Selected >= static_cast(m_Options.size()) - (m_MaxDisplayOptions - m_ScrollLength)) - { - m_TargetY = m_OriginalY - ((m_Options.size() - m_MaxDisplayOptions) * m_OptionHeight); - } - else if (m_Selected >= m_ScrollLength) - { - m_TargetY = m_OriginalY - ((m_Selected - m_ScrollLength) * m_OptionHeight); - } - - if (m_Y != m_TargetY) - { - m_Y += std::ceil((m_TargetY - m_Y) / Config::GetAnimationScaling()); - } -} - -void UI::Menu::Render(SDL_Texture *Target, bool HasFocus) -{ - 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 < static_cast(m_Options.size()); i++, TempY += m_OptionHeight) - { - if (TempY < -m_FontSize) - { - continue; - } - else if (TempY > m_RenderTargetHeight) - { - // This is safe to break the loop for. - break; - } - - // Clear target texture. - m_OptionTarget->Clear(Colors::Transparent); - - if (i == m_Selected) - { - if (HasFocus) - { - // Render the bounding box - UI::RenderBoundingBox(Target, m_X - 4, TempY - 4, m_Width + 8, m_OptionHeight + 8, m_ColorMod); - } - // Render the little rectangle. - SDL::RenderRectFill(m_OptionTarget->Get(), 8, 8, 4, m_OptionHeight - 16, Colors::BlueGreen); - } - // Render text to target. - SDL::Text::Render(m_OptionTarget->Get(), - 24, - (m_OptionHeight / 2) - (m_FontSize / 2), - m_FontSize, - SDL::Text::NO_TEXT_WRAP, - i == m_Selected && HasFocus ? Colors::BlueGreen : 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()); -} - -int UI::Menu::GetSelected(void) const -{ - return m_Selected; -} - -void UI::Menu::SetSelected(int Selected) -{ - m_Selected = Selected; -} - -void UI::Menu::SetWidth(int Width) -{ - m_Width = Width; -} - -void UI::Menu::Reset(void) -{ - m_Selected = 0; - m_Y = m_OriginalY; - m_Options.clear(); -} diff --git a/Source/UI/RenderFunctions.cpp b/Source/UI/RenderFunctions.cpp deleted file mode 100644 index 18c7fe1..0000000 --- a/Source/UI/RenderFunctions.cpp +++ /dev/null @@ -1,53 +0,0 @@ -#include "UI/RenderFunctions.hpp" -#include "Colors.hpp" - -namespace -{ - SDL::SharedTexture s_DialogCorners = nullptr; - SDL::SharedTexture s_MenuBoundingCorners = nullptr; -} // namespace - -void UI::RenderDialogBox(SDL_Texture *Target, int X, int Y, int Width, int Height) -{ - if (!s_DialogCorners) - { - s_DialogCorners = SDL::TextureManager::CreateLoadTexture("DialogCorners", "romfs:/Textures/DialogCorners.png"); - } - - // Top - s_DialogCorners->RenderPart(Target, X, Y, 0, 0, 16, 16); - SDL::RenderRectFill(Target, X + 16, Y, Width - 32, 16, Colors::DialogBox); - s_DialogCorners->RenderPart(Target, (X + Width) - 16, Y, 16, 0, 16, 16); - // Middle - SDL::RenderRectFill(NULL, X, Y + 16, Width, Height - 32, Colors::DialogBox); - // Bottom - s_DialogCorners->RenderPart(Target, X, (Y + Height) - 16, 0, 16, 16, 16); - 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 = {static_cast((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); -} diff --git a/Source/UI/SlideOutPanel.cpp b/Source/UI/SlideOutPanel.cpp deleted file mode 100644 index 6325db7..0000000 --- a/Source/UI/SlideOutPanel.cpp +++ /dev/null @@ -1,90 +0,0 @@ -#include "UI/SlideOutPanel.hpp" -#include "Colors.hpp" -#include "Config.hpp" - -UI::SlideOutPanel::SlideOutPanel(int Width, SlideOutPanel::Side Side) : m_X(Side == Side::Left ? -Width : 1280), m_Width(Width), m_Side(Side) -{ - static int SlidePanelTargetID = 0; - std::string PanelTargetName = "PanelTarget_" + std::to_string(SlidePanelTargetID++); - m_RenderTarget = SDL::TextureManager::CreateLoadTexture(PanelTargetName, Width, 720, SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET); -} - -void UI::SlideOutPanel::Update(bool HasFocus) -{ - double Scaling = Config::GetAnimationScaling(); - - if (!m_IsOpen && m_Side == Side::Left && m_X < 0) - { - m_X -= std::ceil(m_X / Scaling); - } - else if (!m_IsOpen && m_Side == Side::Right && m_X > 1280 - m_Width) - { - m_X += std::ceil((1280.0f - (static_cast(m_Width)) - m_X) / Scaling); - } - else if (m_ClosePanel && m_Side == Side::Left && m_X > -(m_Width)) - { - m_X -= std::ceil((m_Width - m_X) / Scaling); - } - else if (m_ClosePanel && m_Side == Side::Right && m_X < 1280) - { - m_X += std::ceil((1280.0f - m_X) / Scaling); - } - else - { - m_IsOpen = true; - } - - if (HasFocus && m_IsOpen) - { - for (auto &CurrentElement : m_Elements) - { - CurrentElement->Update(HasFocus); - } - } -} - -void UI::SlideOutPanel::Render(SDL_Texture *Target, bool HasFocus) -{ - for (auto &CurrentElement : m_Elements) - { - CurrentElement->Render(m_RenderTarget->Get(), HasFocus); - } - m_RenderTarget->Render(NULL, m_X, 0); -} - -void UI::SlideOutPanel::ClearTarget(void) -{ - m_RenderTarget->Clear(Colors::SlidePanelClear); -} - -void UI::SlideOutPanel::Reset(void) -{ - m_X = m_Side == Side::Left ? -(m_Width) : 1280.0f; - m_IsOpen = false; - m_ClosePanel = false; -} - -void UI::SlideOutPanel::Close(void) -{ - m_ClosePanel = true; -} - -bool UI::SlideOutPanel::IsOpen(void) const -{ - return m_IsOpen; -} - -bool UI::SlideOutPanel::IsClosed(void) const -{ - return m_ClosePanel && (m_Side == Side::Left ? m_X > -(m_Width) : m_X < 1280); -} - -void UI::SlideOutPanel::PushNewElement(std::shared_ptr NewElement) -{ - m_Elements.push_back(NewElement); -} - -SDL_Texture *UI::SlideOutPanel::Get(void) -{ - return m_RenderTarget->Get(); -} diff --git a/Source/UI/TitleTile.cpp b/Source/UI/TitleTile.cpp deleted file mode 100644 index b298b3b..0000000 --- a/Source/UI/TitleTile.cpp +++ /dev/null @@ -1,47 +0,0 @@ -#include "UI/TitleTile.hpp" -#include "Colors.hpp" - -UI::TitleTile::TitleTile(bool IsFavorite, SDL::SharedTexture Icon) : m_IsFavorite(IsFavorite), m_Icon(Icon) {}; - -void UI::TitleTile::Update(bool IsSelected) -{ - if (IsSelected && m_RenderWidth < 164) - { - // I think it's safe to assume both are too small. - m_RenderWidth += 18; - m_RenderHeight += 18; - } - else if (!IsSelected && m_RenderWidth > 128) - { - m_RenderWidth -= 9; - m_RenderHeight -= 9; - } -} - -void UI::TitleTile::Render(SDL_Texture *Target, int X, int Y) -{ - int RenderX = X - ((m_RenderWidth - 128) / 2); - int RenderY = Y - ((m_RenderHeight - 128) / 2); - - m_Icon->RenderStretched(Target, RenderX, RenderY, m_RenderWidth, m_RenderHeight); - if (m_IsFavorite) - { - SDL::Text::Render(Target, RenderX + 4, RenderY + 2, 28, SDL::Text::NO_TEXT_WRAP, Colors::Pink, "\uE017"); - } -} - -void UI::TitleTile::Reset(void) -{ - m_RenderWidth = 128; - m_RenderHeight = 128; -} - -int UI::TitleTile::GetWidth(void) const -{ - return m_RenderWidth; -} - -int UI::TitleTile::GetHeight(void) const -{ - return m_RenderHeight; -} diff --git a/Source/UI/TitleView.cpp b/Source/UI/TitleView.cpp deleted file mode 100644 index efb6650..0000000 --- a/Source/UI/TitleView.cpp +++ /dev/null @@ -1,135 +0,0 @@ -#include "UI/TitleView.hpp" -#include "Colors.hpp" -#include "Config.hpp" -#include "Input.hpp" -#include "Logger.hpp" -#include "UI/RenderFunctions.hpp" -#include - -namespace -{ - constexpr int ICON_ROW_SIZE = 7; -} - -UI::TitleView::TitleView(Data::User *User) : m_User(User) -{ - TitleView::Refresh(); -} - -void UI::TitleView::Update(bool HasFocus) -{ - if (m_TitleTiles.empty()) - { - return; - } - - // Update pulse - if (HasFocus) - { - m_ColorMod.Update(); - } - - // Input. - int TotalTiles = m_TitleTiles.size() - 1; - if (Input::ButtonPressed(HidNpadButton_AnyUp) && (m_Selected -= ICON_ROW_SIZE) < 0) - { - m_Selected = 0; - } - else if (Input::ButtonPressed(HidNpadButton_AnyDown) && (m_Selected += ICON_ROW_SIZE) > TotalTiles) - { - m_Selected = TotalTiles; - } - else if (Input::ButtonPressed(HidNpadButton_AnyLeft) && m_Selected > 0) - { - --m_Selected; - } - else if (Input::ButtonPressed(HidNpadButton_AnyRight) && m_Selected < TotalTiles) - { - ++m_Selected; - } - else if (Input::ButtonPressed(HidNpadButton_L) && (m_Selected -= 21) < 0) - { - m_Selected = 0; - } - else if (Input::ButtonPressed(HidNpadButton_R) && (m_Selected += 21) > TotalTiles) - { - m_Selected = TotalTiles; - } - - double Scaling = Config::GetAnimationScaling(); - if (m_SelectedY > 388.0f) - { - m_Y += std::ceil((388.0f - m_SelectedY) / Scaling); - } - else if (m_SelectedY < 28.0f) - { - m_Y += std::ceil((28.0f - m_SelectedY) / Scaling); - } - - for (size_t i = 0; i < m_TitleTiles.size(); i++) - { - m_TitleTiles.at(i).Update(m_Selected == static_cast(i) ? true : false); - } -} - -void UI::TitleView::Render(SDL_Texture *Target, bool HasFocus) -{ - if (m_TitleTiles.empty()) - { - return; - } - - for (int i = 0, TempY = m_Y; i < static_cast(m_TitleTiles.size()); TempY += 144) - { - int EndRow = i + 7; - for (int j = i, TempX = 32; j < EndRow; j++, i++, TempX += 144) - { - if (i >= static_cast(m_TitleTiles.size())) - { - break; - } - - // Save the X and Y to render the selected tile over the rest. - if (i == m_Selected) - { - m_SelectedX = TempX; - m_SelectedY = TempY; - continue; - } - // Just render - m_TitleTiles.at(i).Render(Target, TempX, TempY); - } - } - // Now render the selected title. - if (HasFocus) - { - SDL::RenderRectFill(Target, m_SelectedX - 23, m_SelectedY - 23, 174, 174, Colors::ClearColor); - UI::RenderBoundingBox(Target, m_SelectedX - 24, m_SelectedY - 24, 176, 176, m_ColorMod); - } - m_TitleTiles.at(m_Selected).Render(Target, m_SelectedX, m_SelectedY); -} - -int UI::TitleView::GetSelected(void) const -{ - return m_Selected; -} - -void UI::TitleView::Refresh(void) -{ - m_TitleTiles.clear(); - for (size_t i = 0; i < m_User->GetTotalDataEntries(); i++) - { - // Get pointer to data from user save index I. - Data::TitleInfo *CurrentTitleInfo = Data::GetTitleInfoByID(m_User->GetApplicationIDAt(i)); - // Emplace is faster than push - m_TitleTiles.emplace_back(Config::IsFavorite(m_User->GetApplicationIDAt(i)), CurrentTitleInfo->GetIcon()); - } -} - -void UI::TitleView::Reset(void) -{ - for (UI::TitleTile &CurrentTile : m_TitleTiles) - { - CurrentTile.Reset(); - } -} diff --git a/include/JKSV.hpp b/include/JKSV.hpp new file mode 100644 index 0000000..ce29379 --- /dev/null +++ b/include/JKSV.hpp @@ -0,0 +1,40 @@ +#pragma once +#include "appstates/AppState.hpp" +#include "sdl.hpp" +#include +#include + +/// @brief Main application class. +class JKSV +{ + public: + /// @brief Initializes JKSV. Initializes services. + JKSV(void); + + /// @brief Exits services. + ~JKSV(); + + /// @brief Returns if initializing was successful and JKSV is running. + /// @return True or false. + bool isRunning(void) const; + + /// @brief Runs JKSV's update routine. + void update(void); + + /// @brief Runs JKSV's render routine. + void render(void); + + /// @brief Pushes a new state to JKSV's state vector. + /// @param newState State to push to vector. + static void pushState(std::shared_ptr newState); + + private: + /// @brief Whether or not initialization was successful and JKSV is still running. + bool m_isRunning = false; + /// @brief Whether or not to print the translation credits. + bool m_showTranslationInfo = false; + /// @brief JKSV icon in upper left corner. + sdl::SharedTexture m_headerIcon = nullptr; + /// @brief Vector of states to update and render. + static inline std::vector> sm_stateVector; +}; diff --git a/Include/JSON.hpp b/include/JSON.hpp similarity index 61% rename from Include/JSON.hpp rename to include/JSON.hpp index 6080677..6b09ff3 100644 --- a/Include/JSON.hpp +++ b/include/JSON.hpp @@ -2,15 +2,15 @@ #include #include -namespace JSON +namespace json { // Use this instead of default json_object using Object = std::unique_ptr; // Use this instead of json_object_from_x. Pass the function and its arguments instead. template - static inline JSON::Object NewObject(json_object *(*Function)(Args...), Args... Arguments) + static inline json::Object newObject(json_object *(*function)(Args...), Args... args) { - return JSON::Object((*Function)(Arguments...), json_object_put); + return json::Object((*function)(args...), json_object_put); } -} // namespace JSON +} // namespace json diff --git a/include/appstates/AppState.hpp b/include/appstates/AppState.hpp new file mode 100644 index 0000000..2bc3dd9 --- /dev/null +++ b/include/appstates/AppState.hpp @@ -0,0 +1,52 @@ +#pragma once +#include "sdl.hpp" +#include + +class AppState +{ + public: + /// @brief Base application state class. + /// @param isClosable Optional. Controls whether or not the state should allow JKSV to close. + AppState(bool isClosable = true); + + /// @brief Ends homebutton and plus locking. + virtual ~AppState(); + + /// @brief Every derived class is required to have this function. + virtual void update(void) = 0; + + /// @brief Every derived class is required to have this function. + virtual void render(void) = 0; + + /// @brief Deactivates state and allows JKSV to purge it from the vector. + void deactivate(void); + + /// @brief Allows a state to be reactivated and pushed to the vector. + void reactivate(void); + + /// @brief Returns if the state is still active. + /// @return Whether state is still active or can be purged. + bool isActive(void) const; + + /// @brief Tells the state it's at the back of the vector and has focus. + void giveFocus(void); + + /// @brief Takes the focus away and tells the state it's no long back(); + void takeFocus(void); + + /// @brief Allows the state to know whether it has focus. + /// @return Whether state has focus or not. + bool hasFocus(void) const; + + /// @brief Returns whether or not JKSV should allow closing while state is active. + /// @return True if closable. False if not. + bool isClosable(void) const; + + private: + /// @brief Stores whether or not the state is currently active. + bool m_isActive = true; + /// @brief Stores whether or not the state has focus. + bool m_hasFocus = false; + /// @brief Stores whether or not the state allows closing. + bool m_isClosable = true; +}; diff --git a/include/appstates/BackupMenuState.hpp b/include/appstates/BackupMenuState.hpp new file mode 100644 index 0000000..1e8f233 --- /dev/null +++ b/include/appstates/BackupMenuState.hpp @@ -0,0 +1,70 @@ +#pragma once +#include "appstates/AppState.hpp" +#include "data/data.hpp" +#include "fslib.hpp" +#include "sdl.hpp" +#include "system/Timer.hpp" +#include "ui/Menu.hpp" +#include "ui/SlideOutPanel.hpp" +#include + +/// @brief This is the state where the user can backup and restore saves. +class BackupMenuState : public AppState +{ + public: + /// @brief Creates a new backup selection state. + /// @param user Pointer to currently selected user. + /// @param titleInfo Pointer to titleInfo of selected title. + /// @param saveType Save data type we're working with. + BackupMenuState(data::User *user, data::TitleInfo *titleInfo, FsSaveDataType saveType); + + /// @brief Destructor. This is required even if it doesn't free or do anything. + ~BackupMenuState() {}; + + /// @brief Required. Inherited virtual function from AppState. + void update(void); + + /// @brief Required. Inherited virtual function from AppState. + void render(void); + + /// @brief Refreshes the directory listing and menu. + void refresh(void); + + private: + /// @brief Pointer to current user. + data::User *m_user = nullptr; + /// @brief Pointer to data for selected title. + data::TitleInfo *m_titleInfo = nullptr; + /// @brief Save data type we're working with. + FsSaveDataType m_saveType; + /// @brief Path to the target directory of the title. + fslib::Path m_directoryPath; + /// @brief Directory listing of the above. + fslib::Directory m_directoryListing; + /// @brief The width of the current title in pixels. + int m_titleWidth = 0; + /// @brief X coordinate to render the games's title at. + int m_titleX = 0; + /// @brief Whether or not the above is too long to be displayed at once and needs to be scrolled. + bool m_titleScrolling = false; + /// @brief Whether or not the scrolling timer was triggered and we should scroll the title string. + bool m_titleScrollTriggered = false; + /// @brief Timer for scrolling the title text if it's too long. + sys::Timer m_titleScrollTimer; + /// @brief Variable that saves whether or not the filesystem has data in it. + bool m_saveHasData = false; + + /// @brief Whether or not anything beyond this point needs to be init'd. Everything here is static and shared by all instances. + static inline bool sm_isInitialized = false; + /// @brief The menu used by all instances of BackupMenuState. + static inline std::unique_ptr m_backupMenu = nullptr; + /// @brief The slide out panel used by all instances of BackupMenuState. + static inline std::unique_ptr m_slidePanel = nullptr; + /// @brief Inner render target so the menu only renders to a certain area. + static inline sdl::SharedTexture m_menuRenderTarget = nullptr; + /// @brief The width of the panels. This is set according to the control guide text. + static inline int m_panelWidth = 0; + + /// @brief Renders the title of the currently targetted game. + void renderTitle(void); +}; diff --git a/include/appstates/ConfirmState.hpp b/include/appstates/ConfirmState.hpp new file mode 100644 index 0000000..830c2f6 --- /dev/null +++ b/include/appstates/ConfirmState.hpp @@ -0,0 +1,131 @@ +#pragma once +#include "JKSV.hpp" +#include "appstates/AppState.hpp" +#include "appstates/ProgressState.hpp" +#include "appstates/TaskState.hpp" +#include "colors.hpp" +#include "input.hpp" +#include "sdl.hpp" +#include "strings.hpp" +#include "system/Task.hpp" +#include "ui/renderFunctions.hpp" +#include +#include +#include + +namespace +{ + // This is the base position on the screen used to center the Yes [A](...) text. + constexpr int YES_X_CENTER_COORDINATE = 460; +} // namespace + +/// @brief Templated class to create confirmation dialogs. +/// @tparam TaskType The type of task spawned on confirmation. Ex: Task, ProgressTask +/// @tparam StateType The state type spawned on confirmation. Ex: TaskState, ProgressState +/// @tparam StructType The type of struct passed to the state on confirmation. +template +class ConfirmState : public AppState +{ + public: + /// @brief All functions passed to this state need to follow this signature: void function( *, std::shared_ptr<>) + using TaskFunction = void (*)(TaskType *, std::shared_ptr); + + /// @brief Constructor for new ConfirmState. + /// @param queryString The string displayed. + /// @param holdRequired Whether or not confirmation requires holding A for three seconds. + /// @param function Function executed on confirmation. + /// @param dataStruct shared_ptr that is passed to function. I tried templating this and it was a nightmare. + ConfirmState(std::string_view queryString, bool holdRequired, TaskFunction function, std::shared_ptr dataStruct) + : AppState(false), m_queryString(queryString.data()), m_yesString(strings::getByName(strings::names::YES_NO, 0)), + m_hold(holdRequired), m_function(function), m_dataStruct(dataStruct) + { + // This is to make centering the Yes [A] string more accurate. + m_yesX = YES_X_CENTER_COORDINATE - (sdl::text::getWidth(22, m_yesString.c_str()) / 2); + m_noX = 820 - (sdl::text::getWidth(22, strings::getByName(strings::names::YES_NO, 1)) / 2); + } + + /// @brief Required even if it does nothing. + ~ConfirmState() {}; + + /// @brief Just updates the ConfirmState. + void update(void) + { + if (input::buttonPressed(HidNpadButton_A) && !m_hold) + { + AppState::deactivate(); + JKSV::pushState(std::make_shared(m_function, m_dataStruct)); + } + else if (input::buttonPressed(HidNpadButton_A) && m_hold) + { + // Get the starting tick count and change the Yes string to the first holding string. + m_startingTickCount = SDL_GetTicks64(); + m_yesString = strings::getByName(strings::names::HOLDING_STRINGS, 0); + } + else if (input::buttonHeld(HidNpadButton_A) && m_hold) + { + uint64_t TickCount = SDL_GetTicks64() - m_startingTickCount; + + // If the TickCount is >= 3 seconds, confirmed. Else, just change the string so we can see we're not holding for nothing? + if (TickCount >= 3000) + { + AppState::deactivate(); + JKSV::pushState(std::make_shared(m_function, m_dataStruct)); + } + else if (TickCount >= 2000) + { + m_yesString = strings::getByName(strings::names::HOLDING_STRINGS, 2); + m_yesX = YES_X_CENTER_COORDINATE - (sdl::text::getWidth(22, m_yesString.c_str()) / 2); + } + else if (TickCount >= 1000) + { + m_yesString = strings::getByName(strings::names::HOLDING_STRINGS, 1); + m_yesX = YES_X_CENTER_COORDINATE - (sdl::text::getWidth(22, m_yesString.c_str()) / 2); + } + } + else if (input::buttonReleased(HidNpadButton_A)) + { + m_yesString = strings::getByName(strings::names::YES_NO, 0); + m_yesX = YES_X_CENTER_COORDINATE - (sdl::text::getWidth(22, m_yesString.c_str()) / 2); + } + else if (input::buttonPressed(HidNpadButton_B)) + { + // Just deactivate and don't do anything. + AppState::deactivate(); + } + } + + /// @brief Renders the state to screen. + void render(void) + { + // Dim background + sdl::renderRectFill(NULL, 0, 0, 1280, 720, colors::DIM_BACKGROUND); + // Render dialog + ui::renderDialogBox(NULL, 280, 262, 720, 256); + // Text + sdl::text::render(NULL, 312, 288, 18, 656, colors::WHITE, m_queryString.c_str()); + // Fake buttons. Maybe real later. + sdl::renderLine(NULL, 280, 454, 999, 454, colors::WHITE); + sdl::renderLine(NULL, 640, 454, 640, 517, colors::WHITE); + // To do: Position this better. Currently brought over from old code. + sdl::text::render(NULL, m_yesX, 476, 22, sdl::text::NO_TEXT_WRAP, colors::WHITE, m_yesString.c_str()); + sdl::text::render(NULL, m_noX, 476, 22, sdl::text::NO_TEXT_WRAP, colors::WHITE, strings::getByName(strings::names::YES_NO, 1)); + } + + private: + // Query string + std::string m_queryString; + // Yes string. + std::string m_yesString; + // X coordinate to render the Yes [A] string to, + int m_yesX = 0; + // X coordinate to render the No [B] string to. + int m_noX = 0; + // Whether or not holding is required to confirm. + bool m_hold; + // For tick counting/holding + uint64_t m_startingTickCount = 0; + // Function + TaskFunction m_function; + // Shared ptr to data to send to confirmation function. + std::shared_ptr m_dataStruct; +}; diff --git a/include/appstates/ExtrasMenuState.hpp b/include/appstates/ExtrasMenuState.hpp new file mode 100644 index 0000000..2633612 --- /dev/null +++ b/include/appstates/ExtrasMenuState.hpp @@ -0,0 +1,27 @@ +#pragma once +#include "appstates/AppState.hpp" +#include "sdl.hpp" +#include "ui/Menu.hpp" + +/// @brief Extras menu. +class ExtrasMenuState : public AppState +{ + public: + /// @brief Constructor. + ExtrasMenuState(void); + + /// @brief Required even if nothing happens. + ~ExtrasMenuState() {}; + + /// @brief Updates the menu. + void update(void); + + /// @brief Renders the menu to screen. + void render(void); + + private: + /// @brief Menu + ui::Menu m_extrasMenu; + /// @brief Render target for menu. + sdl::SharedTexture m_renderTarget; +}; diff --git a/include/appstates/MainMenuState.hpp b/include/appstates/MainMenuState.hpp new file mode 100644 index 0000000..7dc9fc6 --- /dev/null +++ b/include/appstates/MainMenuState.hpp @@ -0,0 +1,56 @@ +#pragma once +#include "appstates/AppState.hpp" +#include "data/data.hpp" +#include "sdl.hpp" +#include "ui/IconMenu.hpp" + +/// @brief The main +class MainMenuState : public AppState +{ + public: + /// @brief Creates and initializes the main menu. + MainMenuState(void); + + /// @brief Required even if it does nothing. + ~MainMenuState() {}; + + /// @brief Runs update routine. + void update(void); + + /// @brief Renders menu to screen. + void render(void); + + /// @brief This function allows other states to signal to this one to refresh the views on next call to update(); + static void refreshViewStates(void); + + private: + /// @brief Render target this state renders to. + sdl::SharedTexture m_renderTarget = nullptr; + + /// @brief The background gradient. + sdl::SharedTexture m_background = nullptr; + + /// @brief Icon for the settings option, + sdl::SharedTexture m_settingsIcon = nullptr; + + /// @brief Icon for the extras option. + sdl::SharedTexture m_extrasIcon = nullptr; + + /// @brief Special menu type that uses icons. + ui::IconMenu m_mainMenu; + + /// @brief Pointer to control guide string so I don't need to call string::getByName every loop. + const char *m_controlGuide = nullptr; + + /// @brief X coordinate of the control guide in the bottom right corner. + int m_controlGuideX; + + /// @brief Vector of pointers to users. + static inline std::vector sm_users; + + /// @brief Vector of views for each user, settings, and extras. + static inline std::vector> sm_states; + + /// @brief For signaling refreshes are needed. + static inline bool sm_refreshNeeded = false; +}; diff --git a/include/appstates/ProgressState.hpp b/include/appstates/ProgressState.hpp new file mode 100644 index 0000000..a4a2383 --- /dev/null +++ b/include/appstates/ProgressState.hpp @@ -0,0 +1,39 @@ +#pragma once +#include "appstates/AppState.hpp" +#include "system/ProgressTask.hpp" +#include +#include + +/// @brief State that shows progress of a task. +class ProgressState : public AppState +{ + public: + /// @brief Constructs a new ProgressState. + /// @param function Function for the task to run. + /// @param args Variadic arguments to be forwarded to the function passed. + /// @note All functions passed to this must follow this signature: void function(sys::ProgressTask *, ) + template + ProgressState(void (*function)(sys::ProgressTask *, Args...), Args... args) + : AppState(false), m_task(function, std::forward(args)...){}; + + /// @brief Required destructor. + ~ProgressState() {}; + + /// @brief Checks if the thread is finished and deactivates this state. + void update(void); + + /// @brief Renders the current progress to screen. + void render(void); + + private: + /// @brief Underlying task that has extra methods for tracking the progress of a task. + sys::ProgressTask m_task; + /// @brief Progress which is saved as a rounded whole number. + size_t m_progress = 0; + /// @brief Width of the green bar in pixels. + size_t m_progressBarWidth = 0; + /// @brief X coordinate of the percentage string. + int m_percentageX = 0; + /// @brief Percentage as a string for printing to screen. + std::string m_percentageString; +}; diff --git a/include/appstates/SaveCreateState.hpp b/include/appstates/SaveCreateState.hpp new file mode 100644 index 0000000..7df9759 --- /dev/null +++ b/include/appstates/SaveCreateState.hpp @@ -0,0 +1,38 @@ +#pragma once +#include "appstates/AppState.hpp" +#include "appstates/TitleSelectCommon.hpp" +#include "data/data.hpp" +#include "ui/Menu.hpp" +#include "ui/SlideOutPanel.hpp" +#include + +/// @brief This is the state that is spawned when CreateSaveData is selected from the user menu. +class SaveCreateState : public AppState +{ + public: + /// @brief Constructs a new SaveCreateState. + /// @param targetUser The target user to create save data for. + /// @param titleSelect The selection view for the user for refreshing and rendering. + SaveCreateState(data::User *targetUser, TitleSelectCommon *titleSelect); + + /// @brief Required destructor. + ~SaveCreateState() {}; + + /// @brief Runs the update routine. + void update(void); + + /// @brief Runs the render routine. + void render(void); + + private: + /// @brief Pointer to target user. + data::User *m_user; + /// @brief Pointer to title selection view for the current user. + TitleSelectCommon *m_titleSelect; + /// @brief Menu populated with every title found on the system. + ui::Menu m_saveMenu; + /// @brief Vector of pointers to the title info. This allows sorting them alphabetically and other things. + std::vector m_titleInfoVector; + /// @brief Shared slide panel all instances use. There's no point in allocating a new one every time. + static inline std::unique_ptr sm_slidePanel = nullptr; +}; diff --git a/include/appstates/SettingsState.hpp b/include/appstates/SettingsState.hpp new file mode 100644 index 0000000..5e946b0 --- /dev/null +++ b/include/appstates/SettingsState.hpp @@ -0,0 +1,29 @@ +#pragma once +#include "appstates/AppState.hpp" +#include "sdl.hpp" +#include "ui/Menu.hpp" + +/// @brief The state for settings. +class SettingsState : public AppState +{ + public: + /// @brief Constructs a new settings state. + SettingsState(void); + + /// @brief Required destructor. + ~SettingsState() {}; + + /// @brief Runs the update routine. + void update(void); + + /// @brief Runs the render routine. + void render(void); + + private: + /// @brief Menu for selecting and toggling settings. + ui::Menu m_settingsMenu; + /// @brief Render target to render to. + sdl::SharedTexture m_renderTarget = nullptr; + /// @brief X coordinate of the control guide in the bottom right corner. + int m_controlGuideX = 0; +}; diff --git a/include/appstates/TaskState.hpp b/include/appstates/TaskState.hpp new file mode 100644 index 0000000..319025a --- /dev/null +++ b/include/appstates/TaskState.hpp @@ -0,0 +1,30 @@ +#pragma once +#include "appstates/AppState.hpp" +#include "system/Task.hpp" +#include + +/// @brief State that spawns a task and allows updates to be printed to screen. +class TaskState : public AppState +{ + public: + /// @brief Constructs and spawns a new TaskState. + /// @param function Function to run in the thread. + /// @param args Variadic templated arguments to forward. + /// @note All functions passed must follow this signature: void function(sys::Task *, ) + template + TaskState(void (*function)(sys::Task *, Args...), Args... args) : AppState(false), m_task(function, std::forward(args)...){}; + + /// @brief Required destructor. + ~TaskState() {}; + + /// @brief Runs update routine. Waits for thread function to signal finish and deactivates. + void update(void); + + /// @brief Run render routine. Prints m_task's status string to screen, basically. + /// @param + void render(void); + + private: + /// @brief Underlying task. + sys::Task m_task; +}; diff --git a/include/appstates/TextTitleSelectState.hpp b/include/appstates/TextTitleSelectState.hpp new file mode 100644 index 0000000..8dcc21b --- /dev/null +++ b/include/appstates/TextTitleSelectState.hpp @@ -0,0 +1,34 @@ +#pragma once +#include "appstates/TitleSelectCommon.hpp" +#include "data/data.hpp" +#include "sdl.hpp" +#include "ui/Menu.hpp" + +/// @brief Text menu title selection state. +class TextTitleSelectState : public TitleSelectCommon +{ + public: + /// @brief Constructs new text menu title selection state. + /// @param user User to construct title select for. + TextTitleSelectState(data::User *user); + + /// @brief Required destructor. + ~TextTitleSelectState() {}; + + /// @brief Runs update routine. + void update(void); + + /// @brief Runs render routine. + void render(void); + + /// @brief Refreshes view for changes. + void refresh(void); + + private: + /// @brief Pointer to user view "belongs" to. + data::User *m_user; + /// @brief Menu to display titles to select from. + ui::Menu m_titleSelectMenu; + /// @brief Target to render to. + sdl::SharedTexture m_renderTarget; +}; diff --git a/include/appstates/TitleSelectCommon.hpp b/include/appstates/TitleSelectCommon.hpp new file mode 100644 index 0000000..606a578 --- /dev/null +++ b/include/appstates/TitleSelectCommon.hpp @@ -0,0 +1,29 @@ +#pragma once +#include "appstates/AppState.hpp" + +/// @brief Class that both view types are derived from. +class TitleSelectCommon : public AppState +{ + public: + /// @brief Constructs a new TitleSelectCommon. Basically just calculates the X coordinate of the control if it wasn't already. + TitleSelectCommon(void); + + /// @brief Required destructor. + virtual ~TitleSelectCommon() {}; + + /// @brief Required, inherited. + virtual void update(void) = 0; + + /// @brief Required, inherited. + virtual void render(void) = 0; + + /// @brief Both derived classes need this function. + virtual void refresh(void) = 0; + + /// @brief Renders the control guide string to the bottom right corner. + void renderControlGuide(void); + + private: + /// @brief X coordinate the control guide is rendered at. + static inline int m_titleControlsX = 0; +}; diff --git a/include/appstates/TitleSelectState.hpp b/include/appstates/TitleSelectState.hpp new file mode 100644 index 0000000..7f81b47 --- /dev/null +++ b/include/appstates/TitleSelectState.hpp @@ -0,0 +1,34 @@ +#pragma once +#include "appstates/TitleSelectCommon.hpp" +#include "data/data.hpp" +#include "sdl.hpp" +#include "ui/TitleView.hpp" + +/// @brief Title select state with icon tiles. +class TitleSelectState : public TitleSelectCommon +{ + public: + /// @brief Constructs new title select state. + /// @param user User the state "belongs" to. + TitleSelectState(data::User *user); + + /// @brief Required destructor. + ~TitleSelectState() {}; + + /// @brief Runs the update routine. + void update(void); + + /// @brief Runs the render routine. + void render(void); + + /// @brief Refreshes the view. + void refresh(void); + + private: + /// @brief Pointer to the user the view belongs to. + data::User *m_user = nullptr; + /// @brief Target to render to. + sdl::SharedTexture m_renderTarget = nullptr; + /// @brief Tiled title selection view. + ui::TitleView m_titleView; +}; diff --git a/include/appstates/UserOptionState.hpp b/include/appstates/UserOptionState.hpp new file mode 100644 index 0000000..ffe2158 --- /dev/null +++ b/include/appstates/UserOptionState.hpp @@ -0,0 +1,36 @@ +#pragma once +#include "appstates/AppState.hpp" +#include "appstates/TitleSelectCommon.hpp" +#include "data/data.hpp" +#include "ui/Menu.hpp" +#include "ui/SlideOutPanel.hpp" +#include + +/// @brief State that allows certain actions to be taken for users. +class UserOptionState : public AppState +{ + public: + /// @brief Constructs a new UserOptionState. + /// @param user Pointer to target user of the state. + /// @param titleSelect Pointer to the selection state for refresh and rendering. + UserOptionState(data::User *user, TitleSelectCommon *titleSelect); + + /// @brief Required destructor. + ~UserOptionState() {}; + + /// @brief Runs the render routine. + void update(void); + + /// @brief Runs the render routine. + void render(void); + + private: + /// @brief Pointer to the target user. + data::User *m_user; + /// @brief Pointer to the selection view. + TitleSelectCommon *m_titleSelect; + /// @brief Menu that displays the options available. + ui::Menu m_userOptionMenu; + /// @brief Slide panel all instances shared. + static inline std::unique_ptr m_menuPanel = nullptr; +}; diff --git a/include/colors.hpp b/include/colors.hpp new file mode 100644 index 0000000..c075b72 --- /dev/null +++ b/include/colors.hpp @@ -0,0 +1,19 @@ +#pragma once +#include "sdl.hpp" + +namespace colors +{ + static constexpr sdl::Color WHITE = {0xFFFFFFFF}; + static constexpr sdl::Color BLACK = {0x000000FF}; + static constexpr sdl::Color RED = {0xFF0000FF}; + static constexpr sdl::Color GREEN = {0x00FF00FF}; + static constexpr sdl::Color BLUE = {0x0099EEFF}; + static constexpr sdl::Color YELLOW = {0xF8FC00FF}; + static constexpr sdl::Color PINK = {0xFF4444FF}; + static constexpr sdl::Color BLUE_GREEN = {0x00FFC5FF}; + static constexpr sdl::Color CLEAR_COLOR = {0x2D2D2DFF}; + static constexpr sdl::Color DIALOG_BOX = {0x505050FF}; + static constexpr sdl::Color DIM_BACKGROUND = {0x00000088}; + static constexpr sdl::Color TRANSPARENT = {0x00000000}; + static constexpr sdl::Color SLIDE_PANEL_CLEAR = {0x000000CC}; +} // namespace colors diff --git a/include/config.hpp b/include/config.hpp new file mode 100644 index 0000000..3ac28c8 --- /dev/null +++ b/include/config.hpp @@ -0,0 +1,75 @@ +#pragma once +#include "fslib.hpp" +#include + +namespace config +{ + /// @brief Attempts to load config from file. If it fails, loads defaults. + void initialize(void); + + /// @brief Resets config to default values. + void resetToDefault(void); + + /// @brief Saves config to file. + void save(void); + + /// @brief Retrieves the config value according to the key passed. + /// @param key Key to retrieve. See config::keys + /// @return Key's value if found. 0 if it is not. + uint8_t getByKey(std::string_view key); + + /// @brief Retrieves value of config at index. + /// @param index Index of value to retrieve. + uint8_t getByIndex(int index); + + /// @brief Returns the working directory. + /// @return Working directory. + fslib::Path getWorkingDirectory(void); + + /// @brief Returns the scaling speed of UI transitions and animations. + /// @return Scaling variable. + double getAnimationScaling(void); + + /// @brief Adds or removes a title from the favorites list. + /// @param applicationID Application ID of title to add or remove. + void addRemoveFavorite(uint64_t applicationID); + + /// @brief Returns if the title is found in the favorites list. + /// @param applicationID Application ID to search for. + /// @return True if found. False if not. + bool isFavorite(uint64_t applicationID); + + /// @brief Adds or removes title from blacklist. + /// @param applicationID Application ID to add or remove. + void addRemoveBlacklist(uint64_t applicationID); + + /// @brief Returns if the title is found in the blacklist. + /// @param applicationID Application ID to search for. + /// @return True if found. False if not. + bool isBlacklisted(uint64_t applicationID); + + // 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 WORKING_DIRECTORY = "WorkingDirectory"; + static constexpr std::string_view INCLUDE_DEVICE_SAVES = "IncludeDeviceSaves"; + static constexpr std::string_view AUTO_BACKUP_ON_RESTORE = "AutoBackupOnRestore"; + static constexpr std::string_view AUTO_NAME_BACKUPS = "AutoNameBackups"; + static constexpr std::string_view AUTO_UPLOAD = "AutoUploadToRemote"; + static constexpr std::string_view HOLD_FOR_DELETION = "HoldForDeletion"; + static constexpr std::string_view HOLD_FOR_RESTORATION = "HoldForRestoration"; + static constexpr std::string_view HOLD_FOR_OVERWRITE = "HoldForOverWrite"; + static constexpr std::string_view ONLY_LIST_MOUNTABLE = "OnlyListMountable"; + static constexpr std::string_view LIST_ACCOUNT_SYS_SAVES = "ListAccountSystemSaves"; + static constexpr std::string_view ALLOW_WRITING_TO_SYSTEM = "AllowSystemSaveWriting"; + static constexpr std::string_view EXPORT_TO_ZIP = "ExportToZip"; + static constexpr std::string_view ZIP_COMPRESSION_LEVEL = "ZipCompressionLevel"; + static constexpr std::string_view TITLE_SORT_TYPE = "TitleSortType"; + static constexpr std::string_view JKSM_TEXT_MODE = "JKSMTextMode"; + static constexpr std::string_view FORCE_ENGLISH = "ForceEnglish"; + static constexpr std::string_view ENABLE_TRASH_BIN = "EnableTrash"; + static constexpr std::string_view UI_ANIMATION_SCALE = "UIAnimationScaling"; + static constexpr std::string_view FAVORITES = "Favorites"; + static constexpr std::string_view BLACKLIST = "BlackList"; + } // namespace keys +} // namespace config diff --git a/include/data/TitleInfo.hpp b/include/data/TitleInfo.hpp new file mode 100644 index 0000000..4337e25 --- /dev/null +++ b/include/data/TitleInfo.hpp @@ -0,0 +1,75 @@ +#pragma once +#include "sdl.hpp" +#include +#include + +namespace data +{ + /// @brief Class that holds data related to titles loaded from the system. + class TitleInfo + { + public: + /// @brief Constructs a TitleInfo instance. Loads control data, icon. + /// @param applicationID Application ID of title to load. + TitleInfo(uint64_t applicationID); + + /// @brief Returns the application ID of the title. + /// @return Title's application ID. + uint64_t getApplicationID(void) const; + + /// @brief Returns the title of the title? + /// @return Title directly from the NACP. + const char *getTitle(void); + + /// @brief Returns the path safe version of the title for file system usage. + /// @return Path safe version of the title. + const char *getPathSafeTitle(void); + + /// @brief Returns the publisher of the title. + /// @return Publisher string from NACP. + const char *getPublisher(void); + + /// @brief Returns the owner ID of the save data. + /// @return Save data owner ID. + uint64_t getSaveDataOwnerID(void) const; + + /// @brief Returns the save data container's base size. + /// @param saveType Type of save data to return. + /// @return Size of baseline save data if applicable. If not, 0. + int64_t getSaveDataSize(FsSaveDataType saveType) const; + + /// @brief Returns the maximum size of the save data container. + /// @param saveType Type of save data to return. + /// @return Maximum size of the save container if applicable. If not, 0. + int64_t getSaveDataSizeMax(FsSaveDataType saveType) const; + + /// @brief Returns the journaling size for the save type passed. + /// @param saveType Save type to return. + /// @return Journal size if applicable. If not, 0. + int64_t getJournalSize(FsSaveDataType saveType) const; + + /// @brief Returns the maximum journal size for the save type passed. + /// @param saveType Save type to return. + /// @return Maximum journal size if applicable. If not, 0. + int64_t getJournalSizeMax(FsSaveDataType saveType) const; + + /// @brief Returns if a title uses the save type passed. + /// @param saveType Save type to check for. + /// @return True on success. False on failure. + bool hasSaveDataType(FsSaveDataType saveType); + + /// @brief Returns a pointer to the icon texture. + /// @return Icon + sdl::SharedTexture getIcon(void) const; + + private: + /// @brief Stores application ID for easier grabbing since JKSV is all pointers. + uint64_t m_applicationID = 0; + /// @brief This is where all the good stuff is. All the data for the title. + NacpStruct m_nacp; + /// @brief This is the path safe version of the title. + char m_pathSafeTitle[0x200] = {0}; + /// @brief Shared icon texture. + 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..850f3b3 --- /dev/null +++ b/include/data/User.hpp @@ -0,0 +1,112 @@ +#pragma once +#include "sdl.hpp" +#include +#include +#include +#include + +namespace data +{ + /// @brief Type used to store save info and play statistics in the vector. Vector is used to preserve the order since I can't use a map without having to extra heap allocate it. + using UserDataEntry = std::pair>; + + /// @brief Class that stores data for the user. + class User + { + public: + /// @brief Constructs a new user with accountID + /// @param accountID AccountID of user. + /// @param saveType Save data type account uses. + User(AccountUid accountID, FsSaveDataType saveType); + + /// @brief This is the constructor used to create the fake system users. + /// @param accountID AccountID to associate with saveType. + /// @param pathSafeNickname The path safe version of the save data since JKSV is in everything the Switch supports. + /// @param iconPath Path to the icon to load for account. + /// @param saveType Save data type of user. + User(AccountUid accountID, std::string_view pathSafeNickname, std::string_view iconPath, FsSaveDataType saveType); + + /// @brief Pushes data to m_userData + /// @param saveInfo SaveDataInfo. + /// @param playStats Play statistics. + void addData(const FsSaveDataInfo &saveInfo, const PdmPlayStatistics &playStats); + + /// @brief Runs the sort algo on the vector. + void sortData(void); + + /// @brief Returns the account ID of the user. + /// @return AccountID + AccountUid getAccountID(void) const; + + /// @brief Returns the save data type the account uses. + /// @return Save data type of the account. + FsSaveDataType getAccountSaveType(void) const; + + /// @brief Returns the account's nickname. + /// @return Account nickname. + const char *getNickname(void) const; + + /// @brief Returns the path safe version of the nickname. + /// @return Path safe nickname. + const char *getPathSafeNickname(void) const; + + /// @brief Returns the total number of entries in the data vector. + /// @return Total number of entries. + size_t getTotalDataEntries(void) const; + + /// @brief Returns the application ID of the title at index. + /// @param index Index of title. + /// @return Application ID if index is valid. 0 if not. + uint64_t getApplicationIDAt(int index) const; + + /// @brief Returns a pointer to the save data info at index. + /// @param index Index of data to fetch. + /// @return Pointer to info if valid. nullptr if out-of-bounds. + FsSaveDataInfo *getSaveInfoAt(int index); + + /// @brief Returns a pointer to the play statistics at index. + /// @param index Index of play statistics to fetch. + /// @return Pointer to play statistics if index is value. nullptr if it's out of bounds. + PdmPlayStatistics *getPlayStatsAt(int index); + + /// @brief Returns a pointer to the save info of applicationID. + /// @param applicationID Application ID to search and fetch for. + /// @return Pointer to save info if found. nullptr if not. + FsSaveDataInfo *getSaveInfoByID(uint64_t applicationID); + + /// @brief Returns a pointer to the play statistics of applicationID + /// @param applicationID Application ID to search and fetch. + /// @return Pointer to play statistics if index is valid. nullptr if it isn't. + PdmPlayStatistics *getPlayStatsByID(uint64_t applicationID); + + /// @brief Returns raw SDL_Texture pointer of icon. + /// @return SDL_Texture of icon. + SDL_Texture *getIcon(void); + + /// @brief Returns the shared texture of icon. Increasing reference count of it. + /// @return Shared icon texture. + sdl::SharedTexture getSharedIcon(void); + + private: + /// @brief Account's ID + AccountUid m_accountID; + /// @brief Type of save data account uses. + FsSaveDataType m_saveType; + /// @brief User's nickname. + char m_nickname[0x20] = {0}; + /// @brief Path safe version of nickname. + char m_pathSafeNickname[0x20] = {0}; + /// @brief User's icon. + sdl::SharedTexture m_icon = nullptr; + /// @brief Vector containing save info and play statistics. + std::vector m_userData; + + /// @brief Loads account structs from system. + /// @param profile AccountProfile struct to write to. + /// @param profileBase AccountProfileBase to write to. + void loadAccount(AccountProfile &profile, AccountProfileBase &profileBase); + + /// @brief Creates a placeholder since something went wrong. + void createAccount(void); + }; +} // namespace data diff --git a/include/data/accountUID.hpp b/include/data/accountUID.hpp new file mode 100644 index 0000000..cf7d8a6 --- /dev/null +++ b/include/data/accountUID.hpp @@ -0,0 +1,28 @@ +#pragma once +#include + +// This solves a lot of problems. +namespace data +{ + static constexpr AccountUid BLANK_ACCOUNT_ID = {0}; +} // namespace data + + +/// @brief Allows comparison of AccountUids since devkitpro decided a struct with two uint64_t's is better than u128 +/// @param accountIDA First account to compare. +/// @param accountIDB Second account to compare. +/// @return True if both account IDs match. +static inline bool operator==(AccountUid accountIDA, AccountUid accountIDB) +{ + return (accountIDA.uid[0] == accountIDB.uid[0]) && (accountIDA.uid[1] == accountIDB.uid[1]); +} + +/// @brief Allows comparison of an AccountUid and a number. +/// @param accountIDA AccountUid to compare. +/// @param accountIDB Number to compare. +/// @return True if they match. False if they don't. +/// @note I'm not 100% sure which uint64_t in the AccountUid struct comes first. I don't know if it's [0][1] or [1][0]. To do: Figure that out. +static inline bool operator==(AccountUid accountIDA, u128 accountIDB) +{ + return accountIDA.uid[0] == (accountIDB >> 64 & 0xFFFFFFFFFFFFFFFF) && accountIDA.uid[1] == (accountIDB & 0xFFFFFFFFFFFFFFFF); +} diff --git a/include/data/data.hpp b/include/data/data.hpp new file mode 100644 index 0000000..236d57f --- /dev/null +++ b/include/data/data.hpp @@ -0,0 +1,31 @@ +#pragma once +#include "data/TitleInfo.hpp" +#include "data/User.hpp" +#include "data/accountUID.hpp" +#include +#include + +namespace data +{ + /// @brief Loads users, applications, and save info from the system. + /// @return True if everything goes fine. False if something goes horribly wrong. + bool initialize(void); + + /// @brief Writes pointers to users to vectorOut + /// @param vectorOut Vector to push the pointers to. + void getUsers(std::vector &vectorOut); + + /// @brief Returns a pointer to the title mapped to applicationID. + /// @param applicationID ApplicationID of title to retrieve. + /// @return Pointer to data. nullptr if it's not found. + data::TitleInfo *getTitleInfoByID(uint64_t applicationID); + + /// @brief Returns a reference to the title info map. + /// @return Reference to TitleInfoMap. + std::unordered_map &getTitleInfoMap(void); + + /// @brief Gets a vector of pointers with all titles with saveType. + /// @param saveType Save data type to check for. + /// @param vectorOut Vector to push pointers to. + void getTitleInfoByType(FsSaveDataType saveType, std::vector &vectorOut); +} // namespace data diff --git a/include/fs/createSaveData.hpp b/include/fs/createSaveData.hpp new file mode 100644 index 0000000..202409d --- /dev/null +++ b/include/fs/createSaveData.hpp @@ -0,0 +1,11 @@ +#pragma once +#include "data/data.hpp" + +namespace fs +{ + /// @brief Creates save data for the target user for the title passed. + /// @param targetUser User to create save data for. + /// @param titleInfo Title to create save data for. + /// @return True on success. False on failure. + bool createSaveDataFor(data::User *targetUser, data::TitleInfo *titleInfo); +} // namespace fs diff --git a/include/fs/fs.hpp b/include/fs/fs.hpp new file mode 100644 index 0000000..87f8f67 --- /dev/null +++ b/include/fs/fs.hpp @@ -0,0 +1,4 @@ +#pragma once +#include "fs/io.hpp" +#include "fs/saveMount.hpp" +#include "fs/zip.hpp" diff --git a/include/fs/io.hpp b/include/fs/io.hpp new file mode 100644 index 0000000..a394735 --- /dev/null +++ b/include/fs/io.hpp @@ -0,0 +1,31 @@ +#pragma once +#include "fslib.hpp" +#include "system/ProgressTask.hpp" +#include + +namespace fs +{ + /// @brief Copies source to destination. + /// @param source Path to source file. + /// @param destination Path to destination. + /// @param journalSize Optional. The size of the journal if data needs to be commited. + /// @param commitDevice Optional. The device to commit to if it's needed. + /// @param Task Optional. Progress tracking task to display progress of operation if needed. + void copyFile(const fslib::Path &source, + const fslib::Path &destination, + uint64_t journalSize = 0, + std::string_view commitDevice = {}, + sys::ProgressTask *Task = nullptr); + + /// @brief Recursively copies source to destination. + /// @param source Source path. + /// @param destination Destination path. + /// @param journalSize Optional. Journal size to be passed to copyFile if data needs to be commited to device. + /// @param commitDevice Optional. Device to commit data to if needed. + /// @param Task Option. Progress tracking task to be passed to copyFile to show progress of operation. + void copyDirectory(const fslib::Path &source, + const fslib::Path &destination, + uint64_t journalSize = 0, + std::string_view commitDevice = {}, + sys::ProgressTask *Task = nullptr); +} // namespace fs diff --git a/include/fs/saveMount.hpp b/include/fs/saveMount.hpp new file mode 100644 index 0000000..030c407 --- /dev/null +++ b/include/fs/saveMount.hpp @@ -0,0 +1,12 @@ +#pragma once +#include +#include + +namespace fs +{ + /// @brief Default mount point used for JKSV for saves. + static constexpr std::string_view DEFAULT_SAVE_MOUNT = "save"; + + /// @brief Same as above, but as a root directory. + static constexpr std::string_view DEFAULT_SAVE_PATH = "save:/"; +} // namespace fs diff --git a/include/fs/zip.hpp b/include/fs/zip.hpp new file mode 100644 index 0000000..89b367e --- /dev/null +++ b/include/fs/zip.hpp @@ -0,0 +1,28 @@ +#pragma once +// Major to do: Stop using minizip and finish the ZipFile class. +#include "fslib.hpp" +#include "system/ProgressTask.hpp" +#include +#include +#include + +namespace fs +{ + /// @brief Copies source to destination. + /// @param source Source file to copy from. + /// @param destination zipFile to write to. + /// @param Task Optional. Task to pass to show progress. + void copyDirectoryToZip(const fslib::Path &source, zipFile destination, sys::ProgressTask *Task = nullptr); + + /// @brief Unzips source to destination. + /// @param source Source zip file to read from. + /// @param destination Destination path to write to. + /// @param journalSize Size of journal for committing data. This is used exclusively for save data. + /// @param commitDevice Device to commit data to. + /// @param task Optional. Task to update to show progress. + void copyZipToDirectory(unzFile source, + const fslib::Path &destination, + uint64_t journalSize, + std::string_view commitDevice, + sys::ProgressTask *Task = nullptr); +} // namespace fs diff --git a/include/input.hpp b/include/input.hpp new file mode 100644 index 0000000..ed227a5 --- /dev/null +++ b/include/input.hpp @@ -0,0 +1,26 @@ +#pragma once +#include + +namespace input +{ + /// @brief Initializes PadState and input. + void initialize(void); + + /// @brief Updates the PadState. + void update(void); + + /// @brief Returns if a button was pressed the current frame, but not the previous. + /// @param button Button to check. + /// @return True if button is pressed. False if it wasn't. + bool buttonPressed(HidNpadButton button); + + /// @brief Returns if the button was pressed or held the previous and current frame. + /// @param button Button to check. + /// @return True if button is held. False if it isn't. + bool buttonHeld(HidNpadButton button); + + /// @brief Returns if the button was pressed or held the previous frame, but not the current. + /// @param button Button to check. + /// @return True if the button was released. False if it wasn't. + bool buttonReleased(HidNpadButton button); +} // namespace input diff --git a/include/keyboard.hpp b/include/keyboard.hpp new file mode 100644 index 0000000..d56d72c --- /dev/null +++ b/include/keyboard.hpp @@ -0,0 +1,15 @@ +#pragma once +#include +#include + +namespace keyboard +{ + /// @brief Gets input using the Switch's keyboard. + /// @param keyboardType Type of keyboard shown. + /// @param defaultText The default text in the keyboard. + /// @param header The header of the keyboard. + /// @param stringOut Pointer to buffer to write to. + /// @param stringLength Size of the buffer to write too. + /// @return True if input was successful and valid. False if it wasn't. + bool getInput(SwkbdType keyboardType, std::string_view defaultText, std::string_view header, char *stringOut, size_t stringLength); +} // namespace keyboard diff --git a/include/logger.hpp b/include/logger.hpp new file mode 100644 index 0000000..1b0222a --- /dev/null +++ b/include/logger.hpp @@ -0,0 +1,12 @@ +#pragma once + +namespace logger +{ + /// @brief Creates and empties the log file + void initialize(void); + + /// @brief Logs a formatted string. + /// @param format Format of string. + /// @param arguments Va arguments. + void log(const char *format, ...); +} // namespace logger diff --git a/include/stringUtil.hpp b/include/stringUtil.hpp new file mode 100644 index 0000000..c95df83 --- /dev/null +++ b/include/stringUtil.hpp @@ -0,0 +1,36 @@ +#pragma once +#include + +namespace stringutil +{ + /// @brief Enum for creating date strings. + enum class DateFormat + { + YearMonthDay, + YearDayMonth + }; + + /// @brief Returns a formatted string as a C++ string. + /// @param format Format of string. + /// @param arguments Arguments for string. + /// @return Formatted C++ string. + std::string getFormattedString(const char *Format, ...); + + /// @brief Replaces and sequence of characters in a string. + /// @param target Target string. + /// @param find Sequence to search for. + /// @param replace What to replace the sequence with. + void replaceInString(std::string &target, std::string_view find, std::string_view replace); + + /// @brief Attempts to sanitize the string for use with the SD card. + /// @param stringIn String to attempt to sanitize. + /// @param stringOut Buffer to write result to. + /// @param stringOutSize Size of buffer. + /// @return True if the string was able to be sanitized. False if it's impossible. + bool sanitizeStringForPath(const char *stringIn, char *stringOut, size_t stringOutSize); + + /// @brief Returns a date string. + /// @param format Optional. Format to use. Default is Year_Month_Day-Time + /// @return Date string. + std::string getDateString(stringutil::DateFormat format = stringutil::DateFormat::YearMonthDay); +} // namespace stringutil diff --git a/include/strings.hpp b/include/strings.hpp new file mode 100644 index 0000000..c319bbc --- /dev/null +++ b/include/strings.hpp @@ -0,0 +1,31 @@ +#pragma once +#include + +namespace strings +{ + // Attempts to load strings from file in RomFS. + bool initialize(void); + // Returns string with name and index. Returns nullptr if string doesn't exist. + const char *getByName(std::string_view name, int index); + // Names of strings to prevent typos. + namespace names + { + static constexpr std::string_view TRANSLATION_INFO = "TranslationInfo"; + static constexpr std::string_view CONTROL_GUIDES = "ControlGuides"; + static constexpr std::string_view SAVE_DATA_TYPES = "SaveDataTypes"; + static constexpr std::string_view MAIN_MENU_NAMES = "MainMenuNames"; + static constexpr std::string_view SETTINGS_MENU = "SettingsMenu"; + static constexpr std::string_view EXTRAS_MENU = "ExtrasMenu"; + static constexpr std::string_view YES_NO = "YesNo"; + static constexpr std::string_view HOLDING_STRINGS = "HoldingStrings"; + static constexpr std::string_view ON_OFF = "OnOff"; + static constexpr std::string_view BACKUP_MENU = "BackupMenu"; + static constexpr std::string_view COPYING_FILES = "CopyingFiles"; + static constexpr std::string_view BACKUPMENU_CONFIRMATIONS = "BackupMenuConfirmations"; + static constexpr std::string_view DELETING_FILES = "DeletingFiles"; + static constexpr std::string_view KEYBOARD_STRINGS = "KeyboardStrings"; + static constexpr std::string_view USER_OPTIONS = "UserOptions"; + static constexpr std::string_view CREATING_SAVE_DATA_FOR = "CreatingSaveDataFor"; + static constexpr std::string_view POP_MESSAGES = "PopMessages"; + } // namespace names +} // namespace strings diff --git a/include/system/ProgressTask.hpp b/include/system/ProgressTask.hpp new file mode 100644 index 0000000..e994c94 --- /dev/null +++ b/include/system/ProgressTask.hpp @@ -0,0 +1,38 @@ +#pragma once +#include "system/Task.hpp" + +namespace sys +{ + /// @brief Derived class of Task that has methods for tracking progress. + class ProgressTask : public sys::Task + { + public: + /// @brief Contstructs a new ProgressTask + /// @param function Function for thread to execute. + /// @param args Arguments to forward to the thread function. + /// @note All functions passed to this must follow this signature: void function(sys::ProgressTask *, ) + template + ProgressTask(void (*function)(sys::ProgressTask *, Args...), Args... args) + : sys::Task(function, this, std::forward(args)...){}; + + /// @brief Resets the progress and sets a new goal. + /// @param goal The goal we all strive for. + void reset(double goal); + + /// @brief Updates the current progress. + /// @param current The current progress value. + void updateCurrent(double current); + + /// @brief Returns the goal value. + /// @return Goal + double getGoal(void) const; + + /// @brief Returns the current progress. + /// @return Current progress. + double getCurrent(void) const; + + private: + // Current value and goal + double m_current, m_goal; + }; +} // namespace sys diff --git a/include/system/Task.hpp b/include/system/Task.hpp new file mode 100644 index 0000000..ebbaf38 --- /dev/null +++ b/include/system/Task.hpp @@ -0,0 +1,63 @@ +#pragma once +#include +#include +#include + +namespace sys +{ + /// @brief Class that runs tasks in a thread and automatically deactivates when finished. + class Task + { + public: + /// @brief Constructs a new task. + /// @param function Function for task to run. + /// @param args Arguments forwarded to thread. + /// @note Functions passed to this class must follow the following signature: void function(sys::Task *, ) + template + Task(void (*function)(sys::Task *, Args...), Args... args) + { + m_thread = std::thread(function, this, std::forward(args)...); + } + + /// @brief Alternate version of the above that allows derived classes to pass themselves to the thread instead. + /// @tparam TaskType Type of task passed to the spawned thread. Ex: ProgressTask instead of Task. + /// @param function Function for task to run. + /// @param task Task passed to function. You don't really need to worry about this. + /// @param args Arguments to forward to the function. + template + Task(void (*function)(TaskType *, Args...), TaskType *task, Args... args) + { + m_thread = std::thread(function, task, std::forward(args)...); + } + + /// @brief Required destructor. + virtual ~Task(); + + /// @brief Returns if the thread has signaled it's finished running. + /// @return True if the thread is still running. False if it isn't. + bool isRunning(void) const; + + /// @brief Allows thread to signal it's finished. + /// @note Spawned task threads must call this when their work is finished. + void finished(void); + + /// @brief Sets the task/threads current status string. Thread safe. + /// @param format Format of string. + /// @param args Arguments for string. + void setStatus(const char *format, ...); + + /// @brief Returns the status string. Thread safe. + /// @return Copy of the status string. + std::string getStatus(void); + + private: + // Whether task is still running. + bool m_isRunning = true; + // Status string the thread can set that the main thread can display. + std::string m_status; + // Mutex so that string doesn't get messed up. + std::mutex m_statusLock; + // Thread + std::thread m_thread; + }; +} // namespace sys diff --git a/include/system/Timer.hpp b/include/system/Timer.hpp new file mode 100644 index 0000000..bf69b8e --- /dev/null +++ b/include/system/Timer.hpp @@ -0,0 +1,40 @@ +#pragma once +#include + +// Apparently system is used already? +namespace sys +{ + /// @brief Class that uses SDL ticks to time things. + class Timer + { + public: + /// @brief Default constructor. + Timer(void) = default; + + /// @brief Constructs a new timer. + /// @param triggerTicks Number of ticks the timer is triggered at. + Timer(uint64_t triggerTicks); + + /// @brief Copy operator. + /// @param timer Timer to copy. + /// @return Reference to copied timer. + Timer &operator=(const Timer &timer); + + /// @brief Starts the timer. + /// @param triggerTicks Number of ticks to trigger at. + void start(uint64_t triggerTicks); + + /// @brief Updates and returns if the timer was triggered. + /// @return True if timer is triggered. False if it isn't. + bool isTriggered(void); + + /// @brief Forces the timer to restart. + void restart(void); + + private: + // Beginning ticks. + uint64_t m_startingTicks; + // How many ticks to trigger the timer. + uint64_t m_triggerTicks; + }; +} // namespace sys diff --git a/include/system/system.hpp b/include/system/system.hpp new file mode 100644 index 0000000..dfbed29 --- /dev/null +++ b/include/system/system.hpp @@ -0,0 +1,4 @@ +#pragma once +#include "system/ProgressTask.hpp" +#include "system/Task.hpp" +#include "system/Timer.hpp" diff --git a/include/ui/ColorMod.hpp b/include/ui/ColorMod.hpp new file mode 100644 index 0000000..1770efd --- /dev/null +++ b/include/ui/ColorMod.hpp @@ -0,0 +1,25 @@ +#pragma once +#include + +namespace ui +{ + /// @brief This class updates and keeps track of a color modifying variable for ui elements that need one. + class ColorMod + { + public: + /// @brief Default constructor. + ColorMod(void) = default; + + /// @brief Updates the color modification variable. + void update(void); + + /// @brief Allows me to use this like it's a uint8_t directly. + operator uint8_t(void) const; + + private: + /// @brief Whether we're adding or subtracting from the color value. + bool m_direction = true; + /// @brief Color 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..6f13069 --- /dev/null +++ b/include/ui/Element.hpp @@ -0,0 +1,25 @@ +#pragma once +#include "sdl.hpp" + +namespace ui +{ + /// @brief Base class for ui elements. + class Element + { + public: + /// @brief Default constructor. + Element(void) = default; + + /// @brief Virtual destructor. + virtual ~Element() {}; + + /// @brief Virtual update method. All derived classes must have this. + /// @param HasFocus Whether or not the state containing the element currently has focus. + virtual void update(bool HasFocus) = 0; + + /// @brief Virtual render method. All derived classes must have this. + /// @param target Target to render to. + /// @param hasFocus Whether or not the containing state has focus. + virtual void render(SDL_Texture *target, bool hasFocus) = 0; + }; +} // namespace ui diff --git a/include/ui/IconMenu.hpp b/include/ui/IconMenu.hpp new file mode 100644 index 0000000..cb9ae36 --- /dev/null +++ b/include/ui/IconMenu.hpp @@ -0,0 +1,45 @@ +#pragma once +#include "ui/Menu.hpp" + +namespace ui +{ + /// @brief This is a hacky derived class to use Menu's code with Icons instead. + class IconMenu : public ui::Menu + { + public: + /// @brief Default constructor. + IconMenu(void) = default; + + /// @brief This constructor calls initialize. + /// @param x X coordinate to render the menu to. + /// @param y Y coordinate to render the menu to. + /// @param renderTargetHeight Height of the render target to calculate how many options can be displayed at once. + IconMenu(int x, int y, int renderTargetHeight); + + /// @brief Required destructor. + ~IconMenu() {}; + + /// @brief Initializes the menu. + /// @param x X coordinate to render the menu to. + /// @param y Y coordinate to render the menu to. + /// @param renderTargetHeight Height of the render target to calculate how many options can be displayed at once. + void initialize(int x, int y, int renderTargetHeight); + + /// @brief Runs the update routine. + /// @param hasFocus Whether or not the containing state has focus. + void update(bool hasFocus); + + /// @brief Runs the render routine. + /// @param target Target to render to. + /// @param hasFocus Whether or not the containing state has focus. + void render(SDL_Texture *target, bool hasFocus); + + /// @brief Adds a new icon to the menu. + /// @param newOption Icon to add. + void addOption(sdl::SharedTexture newOption); + + private: + /// @brief Vector of shared texture pointers to textures used. + std::vector m_options; + }; +} // namespace ui diff --git a/include/ui/Menu.hpp b/include/ui/Menu.hpp new file mode 100644 index 0000000..21129f9 --- /dev/null +++ b/include/ui/Menu.hpp @@ -0,0 +1,85 @@ +#pragma once +#include "sdl.hpp" +#include "ui/ColorMod.hpp" +#include "ui/Element.hpp" +#include +#include + +namespace ui +{ + /// @brief Text based menu class. + class Menu : public ui::Element + { + public: + /// @brief Menu constructor. + /// @param x X coordinate to render to. + /// @param y Y coordinate to render to. + /// @param width Width of the menu options in pixels. + /// @param fontSize Size of the font in pixels to use. + /// @param renderTargetHeight Height of the render target to calculate option height and scrolling. + Menu(int x, int y, int width, int fontSize, int renderTargetHeight); + + /// @brief Required destructor. + ~Menu() {}; + + /// @brief Runs the update routine. + /// @param hasFocus Whether or not the calling state has focus. + void update(bool HasFocus); + + /// @brief Renders the menu. + /// @param target Target to render to. + /// @param hasFocus Whether or not the calling state has focus. + void render(SDL_Texture *target, bool hasFocus); + + /// @brief Adds and option to the menu. + /// @param newOption Option to add to menu. + void addOption(std::string_view NewOption); + + /// @brief Returns the index of the currently selected menu option. + /// @return Index of currently selected option. + int getSelected(void) const; + + /// @brief Sets the selected item. + /// @param selected Value to set selected to. + void setSelected(int selected); + + /// @brief This is a workaround function until I find something better. + /// @param width New width of the menu in pixels. + void setWidth(int width); + + /// @brief Resets the menu and returns it to an empty, default state. + void reset(void); + + protected: + /// @brief X coordinate menu is rendered to. + double m_x; + /// @brief Y coordinate menu is rendered to. + double m_y; + /// @brief Currently selected option. + int m_selected = 0; + /// @brief Color mod for bounding box. + ui::ColorMod m_colorMod; + /// @brief Height of options in pixels. + int m_optionHeight; + /// @brief Target options are rendered to. + sdl::SharedTexture m_optionTarget = nullptr; + + private: + /// @brief This to preserve the original Y coordinate passed. + double m_originalY; + /// @brief The target Y coordinate the menu should be rendered at. + double m_targetY; + /// @brief How many options before scrolling happens. + int m_scrollLength; + /// @brief Width of the menu in pixels. + int m_width; + /// @brief Font size in pixels. + int m_fontSize; + /// @brief Vertical size of the destination render target in pixels. + int m_renderTargetHeight; + /// @brief Maximum number of display options render target can show. + int m_maxDisplayOptions; + /// @brief Vector of options. + std::vector m_options; + }; +} // namespace ui diff --git a/include/ui/PopMessageManager.hpp b/include/ui/PopMessageManager.hpp new file mode 100644 index 0000000..c623b89 --- /dev/null +++ b/include/ui/PopMessageManager.hpp @@ -0,0 +1,61 @@ +#pragma once +#include "system/Timer.hpp" +#include +#include + +namespace ui +{ + // This is the actual struct containing data for a message. + typedef struct + { + // Y coordinate + double m_y; + // Target Y coordinate + double m_targetY; + // Width of the message. + size_t m_width; + // Message string. + std::string m_message; + // Timer. + sys::Timer m_timer; + } PopMessage; + + class PopMessageManager + { + public: + // No copying. + PopMessageManager(const PopMessageManager &) = delete; + PopMessageManager(PopMessageManager &&) = delete; + PopMessageManager &operator=(const PopMessageManager &) = delete; + PopMessageManager &operator=(PopMessageManager &&) = delete; + + /// @brief Updates and processes message queue. + static void update(void); + + /// @brief Renders messages to screen. + static void render(void); + + /// @brief Pushes a new message to the queue for processing. + /// @param displayTicks Number of ticks for the message to be displayed until it is purged. + /// @param format Format of message. + /// @param args Arguments for message. + static void pushMessage(int displayTicks, const char *format, ...); + + /// @brief The default duration of ticks for messages to be shown. + static constexpr int DEFAULT_MESSAGE_TICKS = 2500; + + private: + // Only one instance allowed. + PopMessageManager(void) = default; + // Returns the only instance. + static PopMessageManager &getInstance(void) + { + static PopMessageManager manager; + return manager; + } + // The queue for processing. SDL can't handle things being rendered in multiple threads. + std::vector> m_messageQueue; + // Actual vector of messages + std::vector m_messages; + }; +} // namespace ui diff --git a/include/ui/SlideOutPanel.hpp b/include/ui/SlideOutPanel.hpp new file mode 100644 index 0000000..ff5e64b --- /dev/null +++ b/include/ui/SlideOutPanel.hpp @@ -0,0 +1,76 @@ +#pragma once +#include "sdl.hpp" +#include "ui/Element.hpp" +#include + +namespace ui +{ + class SlideOutPanel : public ui::Element + { + public: + /// @brief Enum for which side of the screen the panel slides from. + enum class Side + { + Left, + Right + }; + + /// @brief Constructor. + /// @param width Width of the panel in pixels. + /// @param side Which side of the screen the panel slides out from. + SlideOutPanel(int width, SlideOutPanel::Side side); + + /// @brief Required destructor. + ~SlideOutPanel() {}; + + /// @brief Runs the update routine. + /// @param hasFocus Whether or not the calling state has focus. + void update(bool hasFocus); + + /// @brief Runs the render routine. + /// @param target Target to render to. + /// @param hasFocus Whether or the the calling state has focus. + void render(SDL_Texture *target, bool hasFocus); + + /// @brief Clears the target to a semi-transparent black. To do: Maybe not hard coded color. + void clearTarget(void); + + /// @brief Resets the panel back to its default state. + void reset(void); + + /// @brief Closes the panel. + void close(void); + + /// @brief Returns if the panel is fully open. + /// @return If the panel is fully open. + bool isOpen(void) const; + + /// @brief Returns if the panel is fully closed. + /// @return If the panel is fully closed. + bool isClosed(void) const; + + /// @brief Pushes a new element to the element vector. + /// @param newElement New element to push. + void pushNewElement(std::shared_ptr newElement); + + /// @brief Returns a pointer to the render target of the panel. + /// @return Raw SDL_Texture pointer to target. + SDL_Texture *get(void); + + private: + /// @brief Bool for whether panel is fully open or not. + bool m_isOpen = false; + /// @brief Whether or not to close panel. + bool m_closePanel = false; + /// @brief Current X coordinate to render to. Panels are always 720 pixels in height so no Y is required. + double m_x; + /// @brief Width of the panel in pixels. + int m_width; + /// @brief Which side the panel is on. + SlideOutPanel::Side m_side; + /// @brief Render target if panel. + sdl::SharedTexture m_renderTarget; + /// @brief Vector of elements. + std::vector> m_elements; + }; +} // namespace ui diff --git a/include/ui/TitleTile.hpp b/include/ui/TitleTile.hpp new file mode 100644 index 0000000..4681bd7 --- /dev/null +++ b/include/ui/TitleTile.hpp @@ -0,0 +1,46 @@ +#pragma once +#include "sdl.hpp" + +namespace ui +{ + /// @brief Tile for Titleview. + class TitleTile + { + public: + /// @brief Constructor. + /// @param isFavorite Whether the title is a favorite and should have the little heart rendered. + /// @param icon Shared texture pointer to the icon. + TitleTile(bool isFavorite, sdl::SharedTexture icon); + + /// @brief Runs the update routine. + /// @param isSelected Whether or not the tile is selected and needs to expand. + void update(bool isSelected); + + /// @brief Runs the render routine. + /// @param target Target to render to. + /// @param x X coordinate to render to. + /// @param y Y coordinate to render to. + void render(SDL_Texture *target, int x, int y); + + /// @brief Resets the width and height of the tile. + void reset(void); + + /// @brief Returns the render width in pixels. + /// @return Render width. + int getWidth(void) const; + + /// @brief Returns the render height in pixels. + /// @return Render height. + int getHeight(void) const; + + private: + /// @brief Width in pixels to render icon at. + int m_renderWidth = 128; + /// @brief Height in pixels to render icon at. + int m_renderHeight = 128; + /// @brief Whether or not the title is a favorite. + bool m_isFavorite = false; + /// @brief Title's icon texture. + sdl::SharedTexture m_icon = nullptr; + }; +} // namespace ui diff --git a/include/ui/TitleView.hpp b/include/ui/TitleView.hpp new file mode 100644 index 0000000..0b64985 --- /dev/null +++ b/include/ui/TitleView.hpp @@ -0,0 +1,57 @@ +#pragma once +#include "data/data.hpp" +#include "sdl.hpp" +#include "ui/ColorMod.hpp" +#include "ui/Element.hpp" +#include "ui/TitleTile.hpp" +#include + +namespace ui +{ + /// @brief Presents a grid of icons to select a title from. + class TitleView : public ui::Element + { + public: + /// @brief Creates a title view using passed user pointer. + /// @param user User to use. + TitleView(data::User *user); + + /// @brief Required destructor. + ~TitleView() {}; + + /// @brief Runs the update routine. + /// @param hasFocus Whether the calling state has focus. + void update(bool hasFocus); + + /// @brief Runs the render routine. + /// @param target Target to render to. + /// @param hasFocus Whether or not the calling state has focus. + void render(SDL_Texture *target, bool hasFocus); + + /// @brief Returns index of the currently selected tile. + /// @return Index of currently selected tile. + int getSelected(void) const; + + /// @brief Forces a refresh of the view. + void refresh(void); + + /// @brief Resets the view to its default, empty state. + void reset(void); + + private: + /// @brief Pointer to user passed. + data::User *m_user = nullptr; + /// @brief Y coordinate. + double m_y = 28.0f; + /// @brief Currently highlighted/selected title. + int m_selected = 0; + /// @brief X coordinate of the currently selected tile so it can be rendered over top of the rest. + double m_selectedX; + /// @brief Y coordinate. Same as above. + double m_selectedY; + /// @brief Color mod for bounding/selection pulse. + ui::ColorMod m_colorMod; + /// @brief Vector of selection tiles. + std::vector m_titleTiles; + }; +} // namespace ui diff --git a/include/ui/renderFunctions.hpp b/include/ui/renderFunctions.hpp new file mode 100644 index 0000000..a8addf7 --- /dev/null +++ b/include/ui/renderFunctions.hpp @@ -0,0 +1,23 @@ +#pragma once +#include "sdl.hpp" + +// These are just functions to render generic parts of the UI. +namespace ui +{ + /// @brief Renders a dialog box + /// @param target Target to render to. + /// @param x X coordinate to render to. + /// @param y Y coordinate to render to. + /// @param width Width of dialog box in pixels. + /// @param height Height of dialog box in pixels. + void renderDialogBox(SDL_Texture *target, int x, int y, int width, int height); + + /// @brief Renders a bounding box. + /// @param target Target to render to. + /// @param x X coordinate to render to. + /// @param y Y coordinate to render to. + /// @param width Width of dialog box in pixels. + /// @param height Height of dialog box in pixels. + /// @param colorMod Color to multiply in rendering. + void renderBoundingBox(SDL_Texture *target, int x, int y, int width, int height, uint8_t colorMod); +} // namespace ui diff --git a/RomFS/Text/DE.json b/romfs/Text/DE.json similarity index 100% rename from RomFS/Text/DE.json rename to romfs/Text/DE.json diff --git a/RomFS/Text/ENGB.json b/romfs/Text/ENGB.json similarity index 100% rename from RomFS/Text/ENGB.json rename to romfs/Text/ENGB.json diff --git a/RomFS/Text/ENUS.json b/romfs/Text/ENUS.json similarity index 97% rename from RomFS/Text/ENUS.json rename to romfs/Text/ENUS.json index e4a0bda..7fd1386 100644 --- a/RomFS/Text/ENUS.json +++ b/romfs/Text/ENUS.json @@ -107,5 +107,9 @@ ], "CreatingSaveDataFor": [ "Creating save data for #%s#..." + ], + "PopMessages": [ + "Save data is empty!", + "Backup is empty!" ] } diff --git a/RomFS/Text/ES.json b/romfs/Text/ES.json similarity index 100% rename from RomFS/Text/ES.json rename to romfs/Text/ES.json diff --git a/RomFS/Text/ES419.json b/romfs/Text/ES419.json similarity index 100% rename from RomFS/Text/ES419.json rename to romfs/Text/ES419.json diff --git a/RomFS/Text/FR.json b/romfs/Text/FR.json similarity index 100% rename from RomFS/Text/FR.json rename to romfs/Text/FR.json diff --git a/RomFS/Text/FRCA.json b/romfs/Text/FRCA.json similarity index 100% rename from RomFS/Text/FRCA.json rename to romfs/Text/FRCA.json diff --git a/RomFS/Text/IT.json b/romfs/Text/IT.json similarity index 100% rename from RomFS/Text/IT.json rename to romfs/Text/IT.json diff --git a/RomFS/Text/JA.json b/romfs/Text/JA.json similarity index 100% rename from RomFS/Text/JA.json rename to romfs/Text/JA.json diff --git a/RomFS/Text/KO.json b/romfs/Text/KO.json similarity index 100% rename from RomFS/Text/KO.json rename to romfs/Text/KO.json diff --git a/RomFS/Text/NL.json b/romfs/Text/NL.json similarity index 100% rename from RomFS/Text/NL.json rename to romfs/Text/NL.json diff --git a/RomFS/Text/PT.json b/romfs/Text/PT.json similarity index 100% rename from RomFS/Text/PT.json rename to romfs/Text/PT.json diff --git a/RomFS/Text/PTBR.json b/romfs/Text/PTBR.json similarity index 100% rename from RomFS/Text/PTBR.json rename to romfs/Text/PTBR.json diff --git a/RomFS/Text/RU.json b/romfs/Text/RU.json similarity index 100% rename from RomFS/Text/RU.json rename to romfs/Text/RU.json diff --git a/RomFS/Text/ZHCN.json b/romfs/Text/ZHCN.json similarity index 100% rename from RomFS/Text/ZHCN.json rename to romfs/Text/ZHCN.json diff --git a/RomFS/Text/ZHTW.json b/romfs/Text/ZHTW.json similarity index 100% rename from RomFS/Text/ZHTW.json rename to romfs/Text/ZHTW.json diff --git a/RomFS/Textures/BCAT.png b/romfs/Textures/BCAT.png similarity index 100% rename from RomFS/Textures/BCAT.png rename to romfs/Textures/BCAT.png diff --git a/RomFS/Textures/Cache.png b/romfs/Textures/Cache.png similarity index 100% rename from RomFS/Textures/Cache.png rename to romfs/Textures/Cache.png diff --git a/RomFS/Textures/DialogCorners.png b/romfs/Textures/DialogCorners.png similarity index 100% rename from RomFS/Textures/DialogCorners.png rename to romfs/Textures/DialogCorners.png diff --git a/RomFS/Textures/ExtrasIcon.png b/romfs/Textures/ExtrasIcon.png similarity index 100% rename from RomFS/Textures/ExtrasIcon.png rename to romfs/Textures/ExtrasIcon.png diff --git a/RomFS/Textures/HeaderIcon.png b/romfs/Textures/HeaderIcon.png similarity index 100% rename from RomFS/Textures/HeaderIcon.png rename to romfs/Textures/HeaderIcon.png diff --git a/RomFS/Textures/Icon.alpha b/romfs/Textures/Icon.alpha similarity index 100% rename from RomFS/Textures/Icon.alpha rename to romfs/Textures/Icon.alpha diff --git a/RomFS/Textures/MenuBackground.png b/romfs/Textures/MenuBackground.png similarity index 100% rename from RomFS/Textures/MenuBackground.png rename to romfs/Textures/MenuBackground.png diff --git a/RomFS/Textures/MenuBounding.png b/romfs/Textures/MenuBounding.png similarity index 100% rename from RomFS/Textures/MenuBounding.png rename to romfs/Textures/MenuBounding.png diff --git a/RomFS/Textures/SettingsIcon.png b/romfs/Textures/SettingsIcon.png similarity index 100% rename from RomFS/Textures/SettingsIcon.png rename to romfs/Textures/SettingsIcon.png diff --git a/RomFS/Textures/SystemSaves.png b/romfs/Textures/SystemSaves.png similarity index 100% rename from RomFS/Textures/SystemSaves.png rename to romfs/Textures/SystemSaves.png diff --git a/source/JKSV.cpp b/source/JKSV.cpp new file mode 100644 index 0000000..bfaa9c5 --- /dev/null +++ b/source/JKSV.cpp @@ -0,0 +1,201 @@ +#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" +#include "sdl.hpp" +#include "strings.hpp" +#include "ui/PopMessageManager.hpp" +#include + +#define ABORT_ON_FAILURE(x) \ + if (!x) \ + { \ + return; \ + } + +namespace +{ + constexpr uint8_t BUILD_MON = 1; + constexpr uint8_t BUILD_DAY = 6; + constexpr uint16_t BUILD_YEAR = 2025; +} // namespace + +template +static bool initializeService(Result (*function)(Args...), const char *serviceName, Args... args) +{ + Result error = (*function)(args...); + if (R_FAILED(error)) + { + logger::log("Error initializing %s: 0x%X.", error); + return false; + } + return true; +} + +JKSV::JKSV(void) +{ + // FsLib + ABORT_ON_FAILURE(fslib::initialize()); + + // This doesn't rely 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()); + + // Services. + // Using administrator so JKSV can still run in Applet mode. + ABORT_ON_FAILURE(initializeService(accountInitialize, "Account", AccountServiceType_Administrator)); + ABORT_ON_FAILURE(initializeService(nsInitialize, "NS")); + ABORT_ON_FAILURE(initializeService(pdmqryInitialize, "PDMQry")); + ABORT_ON_FAILURE(initializeService(plInitialize, "PL", PlServiceType_User)); + ABORT_ON_FAILURE(initializeService(pmshellInitialize, "PMShell")); + ABORT_ON_FAILURE(initializeService(setInitialize, "Set")); + ABORT_ON_FAILURE(initializeService(setsysInitialize, "SetSys")); + ABORT_ON_FAILURE(initializeService(socketInitializeDefault, "Socket")); + + // Input doesn't have anything to return. + input::initialize(); + + // Neither does config. + config::initialize(); + + // Get and create working directory. There isn't much of an FS anymore. + fslib::Path workingDirectory = config::getWorkingDirectory(); + if (!fslib::directoryExists(workingDirectory) && !fslib::createDirectoriesRecursively(workingDirectory)) + { + logger::log("Error creating working directory: %s", fslib::getErrorString()); + return; + } + + // 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); + sdl::text::addColorCharacter(L'<', colors::YELLOW); + sdl::text::addColorCharacter(L'>', colors::GREEN); + sdl::text::addColorCharacter(L'^', colors::PINK); + + // This is to check whether the author wanted credit for their work. + m_showTranslationInfo = std::char_traits::compare(strings::getByName(strings::names::TRANSLATION_INFO, 1), "NULL", 4) != 0; + + // This can't be in an initializer list because it needs SDL initialized. + m_headerIcon = sdl::TextureManager::createLoadTexture("HeaderIcon", "romfs:/Textures/HeaderIcon.png"); + + // Push initial main menu state. + JKSV::pushState(std::make_shared()); + + m_isRunning = true; +} + +JKSV::~JKSV() +{ + socketExit(); + setsysExit(); + setExit(); + pmshellExit(); + plExit(); + pdmqryExit(); + nsExit(); + accountExit(); + sdl::text::exit(); + sdl::exit(); + fslib::exit(); +} + +bool JKSV::isRunning(void) const +{ + return m_isRunning; +} + +void JKSV::update(void) +{ + input::update(); + + if (input::buttonPressed(HidNpadButton_Plus) && !sm_stateVector.empty() && sm_stateVector.back()->isClosable()) + { + m_isRunning = false; + } + + if (!sm_stateVector.empty()) + { + while (!sm_stateVector.back()->isActive()) + { + sm_stateVector.back()->takeFocus(); + sm_stateVector.pop_back(); + sm_stateVector.back()->giveFocus(); + } + sm_stateVector.back()->update(); + } + + // Update pop messages. + ui::PopMessageManager::update(); +} + +void JKSV::render(void) +{ + sdl::frameBegin(colors::CLEAR_COLOR); + // Top and bottom divider lines. + sdl::renderLine(NULL, 30, 88, 1250, 88, colors::WHITE); + sdl::renderLine(NULL, 30, 648, 1250, 648, colors::WHITE); + // Icon + m_headerIcon->render(NULL, 66, 27); + // "JKSV" + sdl::text::render(NULL, 130, 32, 34, sdl::text::NO_TEXT_WRAP, colors::WHITE, "JKSV"); + // Translation info in bottom left. + if (m_showTranslationInfo) + { + sdl::text::render(NULL, + 8, + 680, + 14, + sdl::text::NO_TEXT_WRAP, + colors::WHITE, + strings::getByName(strings::names::TRANSLATION_INFO, 0), + strings::getByName(strings::names::TRANSLATION_INFO, 1)); + } + // Build date + sdl::text::render(NULL, 8, 700, 14, sdl::text::NO_TEXT_WRAP, colors::WHITE, "v. %02d.%02d.%04d", BUILD_MON, BUILD_DAY, BUILD_YEAR); + + // State render loop. + if (!sm_stateVector.empty()) + { + for (auto &CurrentState : sm_stateVector) + { + CurrentState->render(); + } + } + + // Render messages. + ui::PopMessageManager::render(); + + sdl::frameEnd(); +} + +void JKSV::pushState(std::shared_ptr newState) +{ + if (!sm_stateVector.empty()) + { + sm_stateVector.back()->takeFocus(); + } + newState->giveFocus(); + sm_stateVector.push_back(newState); +} diff --git a/source/appstates/AppState.cpp b/source/appstates/AppState.cpp new file mode 100644 index 0000000..7db5723 --- /dev/null +++ b/source/appstates/AppState.cpp @@ -0,0 +1,53 @@ +#include "appstates/AppState.hpp" +#include + +AppState::AppState(bool isClosable) : m_isClosable(isClosable) +{ + if (!m_isClosable) + { + appletBeginBlockingHomeButton(0); + } +} + +AppState::~AppState() +{ + if (!m_isClosable) + { + appletEndBlockingHomeButton(); + } +} + +void AppState::deactivate(void) +{ + m_isActive = false; +} + +void AppState::reactivate(void) +{ + m_isActive = true; +} + +bool AppState::isActive(void) const +{ + return m_isActive; +} + +void AppState::giveFocus(void) +{ + m_hasFocus = true; +} + +void AppState::takeFocus(void) +{ + m_hasFocus = false; +} + +bool AppState::hasFocus(void) const +{ + return m_hasFocus; +} + +bool AppState::isClosable(void) const +{ + return m_isClosable; +} diff --git a/source/appstates/BackupMenuState.cpp b/source/appstates/BackupMenuState.cpp new file mode 100644 index 0000000..a01a4a2 --- /dev/null +++ b/source/appstates/BackupMenuState.cpp @@ -0,0 +1,310 @@ +#include "appstates/BackupMenuState.hpp" +#include "JKSV.hpp" +#include "appstates/ConfirmState.hpp" +#include "appstates/ProgressState.hpp" +#include "colors.hpp" +#include "config.hpp" +#include "fs/fs.hpp" +#include "fslib.hpp" +#include "input.hpp" +#include "keyboard.hpp" +#include "logger.hpp" +#include "sdl.hpp" +#include "stringUtil.hpp" +#include "strings.hpp" +#include "system/system.hpp" +#include "ui/PopMessageManager.hpp" +#include + +// This struct is used to pass data to Restore, Delete, and upload. +struct TargetStruct +{ + // Path of target. + fslib::Path m_targetPath; + // Journal size if commit is needed. + uint64_t m_journalSize; + // Spawning state so refresh can be called. + BackupMenuState *m_spawningState = nullptr; +}; + +// Declarations here. Definitions after class. +// Create new backup in destinationPath +static void createnewBackup(sys::ProgressTask *task, fslib::Path destinationPath, BackupMenuState *spawningState); +// Restores a backup and requires confirmation to do so. Takes a shared_ptr to a TargetStruct. +static void restoreBackup(sys::ProgressTask *task, std::shared_ptr dataStruct); +// Deletes a backup and requires confirmation to do so. Takes a shared_ptr to a TargetStruct. +static void deleteBackup(sys::Task *task, std::shared_ptr dataStruct); + +BackupMenuState::BackupMenuState(data::User *user, data::TitleInfo *titleInfo, FsSaveDataType saveType) + : m_user(user), m_titleInfo(titleInfo), m_saveType(saveType), + m_directoryPath(config::getWorkingDirectory() / m_titleInfo->getPathSafeTitle()), + m_titleWidth(sdl::text::getWidth(22, m_titleInfo->getTitle())), m_titleScrollTimer(3000) +{ + if (!sm_isInitialized) + { + m_panelWidth = sdl::text::getWidth(22, strings::getByName(strings::names::CONTROL_GUIDES, 2)) + 64; + // To do: Give classes an alternate so they don't have to be constructed. + m_backupMenu = std::make_unique(8, 8, m_panelWidth - 24, 24, 600); + m_slidePanel = std::make_unique(m_panelWidth, ui::SlideOutPanel::Side::Right); + m_menuRenderTarget = + sdl::TextureManager::createLoadTexture("backupMenuTarget", m_panelWidth, 600, SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET); + sm_isInitialized = true; + } + + // Check if title needs to be scrolled above the menu or just set the X position. + if (m_titleWidth >= m_panelWidth) + { + m_titleScrolling = true; + m_titleX = 8; + } + else + { + m_titleX = (m_panelWidth / 2) - (m_titleWidth / 2); + } + + // Check if there's currently any data to backup to prevent blanks. + { + fslib::Directory saveCheck(fs::DEFAULT_SAVE_PATH); + m_saveHasData = saveCheck.getCount() == 0; + } + + BackupMenuState::refresh(); +} + +void BackupMenuState::update(void) +{ + if (input::buttonPressed(HidNpadButton_A) && m_backupMenu->getSelected() == 0 && m_saveHasData) + { + // get name for backup. + char backupName[0x81] = {0}; + + // Set backup to default. + std::snprintf(backupName, 0x80, "%s - %s", m_user->getPathSafeNickname(), stringutil::getDateString().c_str()); + + if (!input::buttonHeld(HidNpadButton_ZR) && + !keyboard::getInput(SwkbdType_QWERTY, backupName, strings::getByName(strings::names::KEYBOARD_STRINGS, 0), backupName, 0x80)) + { + return; + } + // To do: This isn't a good way to check for this... Check to make sure zip has zip extension. + if (config::getByKey(config::keys::EXPORT_TO_ZIP) && std::strstr(backupName, ".zip") == NULL) + { + // To do: I should check this. + std::strcat(backupName, ".zip"); + } + else if (!config::getByKey(config::keys::EXPORT_TO_ZIP) && !std::strstr(backupName, ".zip") && + !fslib::directoryExists(m_directoryPath / backupName) && !fslib::createDirectory(m_directoryPath / backupName)) + { + return; + } + // Push the task. + JKSV::pushState(std::make_shared(createnewBackup, m_directoryPath / backupName, this)); + } + else if (input::buttonPressed(HidNpadButton_A) && m_backupMenu->getSelected() == 0 && !m_saveHasData) + { + ui::PopMessageManager::pushMessage(ui::PopMessageManager::DEFAULT_MESSAGE_TICKS, strings::getByName(strings::names::POP_MESSAGES, 0)); + } + else if (input::buttonPressed(HidNpadButton_Y) && m_backupMenu->getSelected() > 0 && + (m_saveType != FsSaveDataType_System || config::getByKey(config::keys::ALLOW_WRITING_TO_SYSTEM))) + { + // Need to account for new at the top. + int selected = m_backupMenu->getSelected() - 1; + + std::shared_ptr dataStruct(new TargetStruct); + dataStruct->m_targetPath = m_directoryPath / m_directoryListing[selected]; + dataStruct->m_journalSize = m_titleInfo->getJournalSize(m_saveType); + + std::string queryString = + stringutil::getFormattedString(strings::getByName(strings::names::BACKUPMENU_CONFIRMATIONS, 0), m_directoryListing[selected]); + + JKSV::pushState( + std::make_shared>(queryString, + config::getByKey(config::keys::HOLD_FOR_RESTORATION), + restoreBackup, + dataStruct)); + } + else if (input::buttonPressed(HidNpadButton_X) && m_backupMenu->getSelected() > 0) + { + // Selected needs to be offset by one to account for New + int selected = m_backupMenu->getSelected() - 1; + + // Create struct to pass. + std::shared_ptr dataStruct(new TargetStruct); + dataStruct->m_targetPath = m_directoryPath / m_directoryListing[selected]; + dataStruct->m_spawningState = this; + + // get the string. + std::string queryString = + stringutil::getFormattedString(strings::getByName(strings::names::BACKUPMENU_CONFIRMATIONS, 1), m_directoryListing[selected]); + + // Create/push new state. + JKSV::pushState(std::make_shared>(queryString, + config::getByKey(config::keys::HOLD_FOR_DELETION), + deleteBackup, + dataStruct)); + } + else if (input::buttonPressed(HidNpadButton_B)) + { + fslib::closeFileSystem(fs::DEFAULT_SAVE_MOUNT); + m_slidePanel->close(); + } + else if (m_slidePanel->isClosed()) + { + m_slidePanel->reset(); + AppState::deactivate(); + } + + m_slidePanel->update(AppState::hasFocus()); + // This state bypasses the Slideout panel's normal behavior because it kind of has to. + m_backupMenu->update(AppState::hasFocus()); +} + +void BackupMenuState::render(void) +{ + // Clear panel target. + m_slidePanel->clearTarget(); + // render the current title's name. + BackupMenuState::renderTitle(); + sdl::renderLine(m_slidePanel->get(), 10, 42, m_panelWidth - 20, 42, colors::WHITE); + sdl::renderLine(m_slidePanel->get(), 10, 648, m_panelWidth - 20, 648, colors::WHITE); + sdl::text::render(m_slidePanel->get(), + 32, + 673, + 22, + sdl::text::NO_TEXT_WRAP, + colors::WHITE, + strings::getByName(strings::names::CONTROL_GUIDES, 2)); + + // Clear menu target. + m_menuRenderTarget->clear(colors::TRANSPARENT); + // render menu to it. + m_backupMenu->render(m_menuRenderTarget->get(), AppState::hasFocus()); + // render it to panel target. + m_menuRenderTarget->render(m_slidePanel->get(), 0, 43); + m_slidePanel->render(NULL, AppState::hasFocus()); +} + +void BackupMenuState::refresh(void) +{ + m_directoryListing.open(m_directoryPath); + if (!m_directoryListing.isOpen()) + { + return; + } + + m_backupMenu->reset(); + m_backupMenu->addOption(strings::getByName(strings::names::BACKUP_MENU, 0)); + for (int64_t i = 0; i < m_directoryListing.getCount(); i++) + { + m_backupMenu->addOption(m_directoryListing[i]); + } +} + +void BackupMenuState::renderTitle(void) +{ + SDL_Texture *SlidePanelTarget = m_slidePanel->get(); + + if (m_titleScrolling && m_titleScrollTriggered && m_titleX > -(m_titleWidth + 8)) + { + m_titleX -= 2; + } + else if (m_titleScrolling && m_titleScrollTriggered && m_titleX <= -(m_titleWidth + 8)) + { + m_titleX = 8; + m_titleScrollTriggered = false; + m_titleScrollTimer.restart(); + } + else if (m_titleScrolling && m_titleScrollTimer.isTriggered()) + { + m_titleScrollTriggered = true; + } + + if (m_titleScrolling && m_titleScrollTriggered) + { + // This is just a trick, or maybe the only way to accomplish this. Either way, it works. + // render title first time. + sdl::text::render(SlidePanelTarget, m_titleX, 8, 22, sdl::text::NO_TEXT_WRAP, colors::WHITE, m_titleInfo->getTitle()); + // render it again following the first. + sdl::text::render(SlidePanelTarget, + m_titleX + m_titleWidth + 16, + 8, + 22, + sdl::text::NO_TEXT_WRAP, + colors::WHITE, + m_titleInfo->getTitle()); + } + else + { + sdl::text::render(SlidePanelTarget, m_titleX, 8, 22, sdl::text::NO_TEXT_WRAP, colors::WHITE, m_titleInfo->getTitle()); + } +} + +// This is the function to create new backups. +static void createnewBackup(sys::ProgressTask *task, fslib::Path destinationPath, BackupMenuState *spawningState) +{ + // This extension search is lazy and needs to be revised. + if (config::getByKey(config::keys::EXPORT_TO_ZIP) || std::strstr(destinationPath.cString(), ".zip") != NULL) + { + zipFile newBackup = zipOpen64(destinationPath.cString(), APPEND_STATUS_CREATE); + fs::copyDirectoryToZip(fs::DEFAULT_SAVE_PATH, newBackup, task); + zipClose(newBackup, NULL); + } + else + { + fs::copyDirectory(fs::DEFAULT_SAVE_PATH, destinationPath, 0, {}, task); + } + spawningState->refresh(); + task->finished(); +} + +static void restoreBackup(sys::ProgressTask *task, std::shared_ptr dataStruct) +{ + // Wipe the save root first. + if (!fslib::deleteDirectoryRecursively(fs::DEFAULT_SAVE_PATH)) + { + logger::log("Error restoring save: %s", fslib::getErrorString()); + task->finished(); + return; + } + + if (fslib::directoryExists(dataStruct->m_targetPath)) + { + fs::copyDirectory(dataStruct->m_targetPath, fs::DEFAULT_SAVE_PATH, dataStruct->m_journalSize, fs::DEFAULT_SAVE_MOUNT, task); + } + else if (std::strstr(dataStruct->m_targetPath.cString(), ".zip") != NULL) + { + unzFile targetZip = unzOpen64(dataStruct->m_targetPath.cString()); + if (!targetZip) + { + logger::log("Error opening zip for reading."); + task->finished(); + return; + } + fs::copyZipToDirectory(targetZip, fs::DEFAULT_SAVE_PATH, dataStruct->m_journalSize, fs::DEFAULT_SAVE_MOUNT, task); + unzClose(targetZip); + } + else + { + fs::copyFile(dataStruct->m_targetPath, fs::DEFAULT_SAVE_PATH, dataStruct->m_journalSize, fs::DEFAULT_SAVE_MOUNT, task); + } + task->finished(); +} + +static void deleteBackup(sys::Task *task, std::shared_ptr dataStruct) +{ + if (task) + { + task->setStatus(strings::getByName(strings::names::DELETING_FILES, 0), dataStruct->m_targetPath.cString()); + } + + if (fslib::directoryExists(dataStruct->m_targetPath) && !fslib::deleteDirectoryRecursively(dataStruct->m_targetPath)) + { + logger::log("Error deleting folder backup: %s", fslib::getErrorString()); + } + else if (!fslib::deleteFile(dataStruct->m_targetPath)) + { + logger::log("Error deleting backup: %s", fslib::getErrorString()); + } + dataStruct->m_spawningState->refresh(); + task->finished(); +} diff --git a/source/appstates/ExtrasMenuState.cpp b/source/appstates/ExtrasMenuState.cpp new file mode 100644 index 0000000..59d67e7 --- /dev/null +++ b/source/appstates/ExtrasMenuState.cpp @@ -0,0 +1,40 @@ +#include "appstates/ExtrasMenuState.hpp" +#include "colors.hpp" +#include "input.hpp" +#include "strings.hpp" +#include + +namespace +{ + // This target is shared be a lot of states. + constexpr std::string_view SECONDARY_TARGET = "SecondaryTarget"; +} // namespace + +ExtrasMenuState::ExtrasMenuState(void) + : m_extrasMenu(32, 8, 1000, 24, 555), + m_renderTarget(sdl::TextureManager::createLoadTexture(SECONDARY_TARGET, 1080, 555, SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET)) +{ + const char *extrasString = nullptr; + int currentString = 0; + while ((extrasString = strings::getByName(strings::names::EXTRAS_MENU, currentString++)) != nullptr) + { + m_extrasMenu.addOption(extrasString); + } +} + +void ExtrasMenuState::update(void) +{ + m_extrasMenu.update(AppState::hasFocus()); + + if (input::buttonPressed(HidNpadButton_B)) + { + AppState::deactivate(); + } +} + +void ExtrasMenuState::render(void) +{ + m_renderTarget->clear(colors::TRANSPARENT); + m_extrasMenu.render(m_renderTarget->get(), AppState::hasFocus()); + m_renderTarget->render(NULL, 201, 91); +} diff --git a/source/appstates/MainMenuState.cpp b/source/appstates/MainMenuState.cpp new file mode 100644 index 0000000..a820d0a --- /dev/null +++ b/source/appstates/MainMenuState.cpp @@ -0,0 +1,102 @@ +#include "appstates/MainMenuState.hpp" +#include "JKSV.hpp" +#include "appstates/ExtrasMenuState.hpp" +#include "appstates/SettingsState.hpp" +#include "appstates/TextTitleSelectState.hpp" +#include "appstates/TitleSelectCommon.hpp" +#include "appstates/TitleSelectState.hpp" +#include "appstates/UserOptionState.hpp" +#include "colors.hpp" +#include "config.hpp" +#include "input.hpp" +#include "logger.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_mainMenu(50, 15, 555), + m_controlGuide(strings::getByName(strings::names::CONTROL_GUIDES, 0)), m_controlGuideX(1220 - sdl::text::getWidth(22, m_controlGuide)) +{ + // Fetch user list. + data::getUsers(sm_users); + + // Loop through add user's icon to menu and create states. + for (size_t i = 0; i < sm_users.size(); i++) + { + m_mainMenu.addOption(sm_users.at(i)->getSharedIcon()); + + if (config::getByKey(config::keys::JKSM_TEXT_MODE)) + { + sm_states.push_back(std::make_shared(sm_users.at(i))); + } + else + { + sm_states.push_back(std::make_shared(sm_users.at(i))); + } + } + // Add the settings and extras. + sm_states.push_back(std::make_shared()); + sm_states.push_back(std::make_shared()); + + // Create icons for the other two. + m_settingsIcon = sdl::TextureManager::createLoadTexture("SettingsIcon", "romfs:/Textures/SettingsIcon.png"); + m_extrasIcon = sdl::TextureManager::createLoadTexture("ExtrasIcon", "romfs:/Textures/ExtrasIcon.png"); + + // Finally add them to the end. + m_mainMenu.addOption(m_settingsIcon); + m_mainMenu.addOption(m_extrasIcon); +} + +void MainMenuState::update(void) +{ + m_mainMenu.update(AppState::hasFocus()); + + int selected = m_mainMenu.getSelected(); + + if (input::buttonPressed(HidNpadButton_A) && selected < static_cast(sm_users.size()) && + sm_users.at(selected)->getTotalDataEntries() > 0) + { + sm_states.at(selected)->reactivate(); + JKSV::pushState(sm_states.at(selected)); + } + else if (input::buttonPressed(HidNpadButton_A) && selected >= static_cast(sm_users.size())) + { + sm_states.at(selected)->reactivate(); + JKSV::pushState(sm_states.at(selected)); + } + else if (input::buttonPressed(HidNpadButton_X) && selected < static_cast(sm_users.size())) + { + // Get pointers to data the user option state needs. + data::User *targetUser = sm_users.at(selected); + TitleSelectCommon *targetTitleSelect = reinterpret_cast(sm_states.at(selected).get()); + + JKSV::pushState(std::make_shared(targetUser, targetTitleSelect)); + } +} + +void MainMenuState::render(void) +{ + // Clear render target by rendering background to it. + m_background->render(m_renderTarget->get(), 0, 0); + // render menu. + m_mainMenu.render(m_renderTarget->get(), AppState::hasFocus()); + // render target to screen. + m_renderTarget->render(NULL, 0, 91); + + // render next state for current user and control guide if this state has focus. + if (AppState::hasFocus()) + { + sm_states.at(m_mainMenu.getSelected())->render(); + sdl::text::render(NULL, m_controlGuideX, 673, 22, sdl::text::NO_TEXT_WRAP, colors::WHITE, m_controlGuide); + } +} + +void MainMenuState::refreshViewStates(void) +{ + for (size_t i = 0; i < sm_users.size(); i++) + { + sm_users.at(i)->sortData(); + std::static_pointer_cast(sm_states.at(i))->refresh(); + } +} diff --git a/source/appstates/ProgressState.cpp b/source/appstates/ProgressState.cpp new file mode 100644 index 0000000..e54da42 --- /dev/null +++ b/source/appstates/ProgressState.cpp @@ -0,0 +1,34 @@ +#include "appstates/ProgressState.hpp" +#include "colors.hpp" +#include "input.hpp" +#include "sdl.hpp" +#include "stringUtil.hpp" +#include "strings.hpp" +#include "ui/renderFunctions.hpp" +#include + +void ProgressState::update(void) +{ + if (!m_task.isRunning()) + { + AppState::deactivate(); + } + + m_progressBarWidth = std::ceil(656.0f * m_task.getCurrent()); + m_progress = std::ceil(m_task.getCurrent() * 100); + m_percentageString = stringutil::getFormattedString("%u", m_progress); + m_percentageX = 640 - (sdl::text::getWidth(18, m_percentageString.c_str())); +} + +void ProgressState::render(void) +{ + // This will dim the background. + sdl::renderRectFill(NULL, 0, 0, 1280, 720, colors::DIM_BACKGROUND); + + // Render the dialog and little loading bar thingy. + ui::renderDialogBox(NULL, 280, 262, 720, 256); + sdl::text::render(NULL, 312, 288, 18, 648, colors::WHITE, m_task.getStatus().c_str()); + sdl::renderRectFill(NULL, 312, 462, 656, 32, colors::BLACK); + sdl::renderRectFill(NULL, 312, 462, m_progressBarWidth, 32, colors::GREEN); + sdl::text::render(NULL, m_percentageX, 468, 18, sdl::text::NO_TEXT_WRAP, colors::WHITE, "%s%%", m_percentageString.c_str()); +} diff --git a/source/appstates/SaveCreateState.cpp b/source/appstates/SaveCreateState.cpp new file mode 100644 index 0000000..ca09d50 --- /dev/null +++ b/source/appstates/SaveCreateState.cpp @@ -0,0 +1,106 @@ +#include "appstates/SaveCreateState.hpp" +#include "JKSV.hpp" +#include "appstates/TaskState.hpp" +#include "data/data.hpp" +#include "input.hpp" +#include "logger.hpp" +#include "strings.hpp" +#include "system/Task.hpp" +#include +#include +#include +#include + +// This sorts the vector alphabetically so stuff is easier to find +static bool compareInfo(data::TitleInfo *infoA, data::TitleInfo *infoB) +{ + const char *titleA = infoA->getTitle(); + const char *titleB = infoB->getTitle(); + + size_t titleALength = std::char_traits::length(titleA); + size_t titleBLength = std::char_traits::length(titleB); + size_t shortestTitle = titleALength < titleBLength ? titleALength : titleBLength; + // To do: This doesn't take into account which is the shortest title. This can still go out-of-bounds. + for (size_t i = 0, j = 0; i < shortestTitle;) + { + uint32_t codepointA = 0; + uint32_t codepointB = 0; + + ssize_t unitCountA = decode_utf8(&codepointA, reinterpret_cast(&titleA[i])); + ssize_t unitCountB = decode_utf8(&codepointB, reinterpret_cast(&titleB[j])); + + if (unitCountA <= 0 || unitCountB <= 0) + { + return false; + } + + if (codepointA != codepointB) + { + return codepointA < codepointB; + } + + i += unitCountA; + j += unitCountB; + } + return false; +} + +// This attempts to create the save data for the given user. It will fail if it already exists. +static void createSaveDataFor(sys::Task *task, data::User *targetUser, data::TitleInfo *titleInfo) +{ + // Set task status. + task->setStatus(strings::getByName(strings::names::CREATING_SAVE_DATA_FOR, 0), titleInfo->getTitle()); + + task->finished(); +} + +SaveCreateState::SaveCreateState(data::User *targetUser, TitleSelectCommon *titleSelect) + : m_user(targetUser), m_titleSelect(titleSelect), m_saveMenu(8, 8, 624, 22, 720) +{ + // If the panel is null, create it. + if (!sm_slidePanel) + { + // Create panel and menu. + sm_slidePanel = std::make_unique(640, ui::SlideOutPanel::Side::Right); + } + + // Get title info vector and copy titles to menu. + data::getTitleInfoByType(m_user->getAccountSaveType(), m_titleInfoVector); + + // Sort it by alpha + std::sort(m_titleInfoVector.begin(), m_titleInfoVector.end(), compareInfo); + + for (size_t i = 0; i < m_titleInfoVector.size(); i++) + { + m_saveMenu.addOption(m_titleInfoVector.at(i)->getTitle()); + } +} + +void SaveCreateState::update(void) +{ + sm_slidePanel->update(AppState::hasFocus()); + m_saveMenu.update(AppState::hasFocus()); + + if (input::buttonPressed(HidNpadButton_A)) + { + data::TitleInfo *targetTitle = m_titleInfoVector.at(m_saveMenu.getSelected()); + JKSV::pushState(std::make_shared(createSaveDataFor, m_user, targetTitle)); + } + else if (input::buttonPressed(HidNpadButton_B)) + { + sm_slidePanel->close(); + } + else if (sm_slidePanel->isClosed()) + { + sm_slidePanel->reset(); + AppState::deactivate(); + } +} + +void SaveCreateState::render(void) +{ + // Clear slide target, render menu, render slide to frame buffer. + sm_slidePanel->clearTarget(); + m_saveMenu.render(sm_slidePanel->get(), AppState::hasFocus()); + sm_slidePanel->render(NULL, AppState::hasFocus()); +} diff --git a/source/appstates/SettingsState.cpp b/source/appstates/SettingsState.cpp new file mode 100644 index 0000000..c619fbe --- /dev/null +++ b/source/appstates/SettingsState.cpp @@ -0,0 +1,65 @@ +#include "appstates/SettingsState.hpp" +#include "colors.hpp" +#include "config.hpp" +#include "input.hpp" +#include "stringUtil.hpp" +#include "strings.hpp" + +namespace +{ + // All of these states share the same render target. + constexpr std::string_view SECONDARY_TARGET = "SecondaryTarget"; +} // namespace + +static inline const char *getvalueText(uint8_t value) +{ + return value == 1 ? strings::getByName(strings::names::ON_OFF, 0) : strings::getByName(strings::names::ON_OFF, 1); +} + +SettingsState::SettingsState(void) + : m_settingsMenu(32, 8, 1000, 24, 555), + m_renderTarget(sdl::TextureManager::createLoadTexture(SECONDARY_TARGET, 1080, 555, SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET)), + m_controlGuideX(1220 - sdl::text::getWidth(22, strings::getByName(strings::names::CONTROL_GUIDES, 3))) +{ + // Add the first two, because they don't have values to display. + m_settingsMenu.addOption(strings::getByName(strings::names::SETTINGS_MENU, 0)); + m_settingsMenu.addOption(strings::getByName(strings::names::SETTINGS_MENU, 1)); + + int currentString = 2; + const char *settingsString = nullptr; + while (currentString < 16 && (settingsString = strings::getByName(strings::names::SETTINGS_MENU, currentString++)) != nullptr) + { + m_settingsMenu.addOption(stringutil::getFormattedString(settingsString, getvalueText(config::getByIndex(currentString - 1)))); + } + // Add the scaling + m_settingsMenu.addOption( + stringutil::getFormattedString(strings::getByName(strings::names::SETTINGS_MENU, 17), config::getAnimationScaling())); +} + +void SettingsState::update(void) +{ + m_settingsMenu.update(AppState::hasFocus()); + + if (input::buttonPressed(HidNpadButton_B)) + { + AppState::deactivate(); + } +} + +void SettingsState::render(void) +{ + m_renderTarget->clear(colors::TRANSPARENT); + m_settingsMenu.render(m_renderTarget->get(), AppState::hasFocus()); + m_renderTarget->render(NULL, 201, 91); + + if (AppState::hasFocus()) + { + sdl::text::render(NULL, + m_controlGuideX, + 673, + 22, + sdl::text::NO_TEXT_WRAP, + colors::WHITE, + strings::getByName(strings::names::CONTROL_GUIDES, 3)); + } +} diff --git a/source/appstates/TaskState.cpp b/source/appstates/TaskState.cpp new file mode 100644 index 0000000..486c25e --- /dev/null +++ b/source/appstates/TaskState.cpp @@ -0,0 +1,23 @@ +#include "appstates/TaskState.hpp" +#include "colors.hpp" +#include "sdl.hpp" + +void TaskState::update(void) +{ + if (!m_task.isRunning()) + { + AppState::deactivate(); + } +} + +void TaskState::render(void) +{ + // Grab task string. + std::string status = m_task.getStatus(); + // Center so it looks perty + int statusX = 640 - (sdl::text::getWidth(24, status.c_str()) / 2); + // Dim the background states. + sdl::renderRectFill(NULL, 0, 0, 1280, 720, colors::DIM_BACKGROUND); + // Render the status. + sdl::text::render(NULL, statusX, 351, 24, sdl::text::NO_TEXT_WRAP, colors::WHITE, status.c_str()); +} diff --git a/source/appstates/TextTitleSelectState.cpp b/source/appstates/TextTitleSelectState.cpp new file mode 100644 index 0000000..e3ef7b0 --- /dev/null +++ b/source/appstates/TextTitleSelectState.cpp @@ -0,0 +1,63 @@ +#include "appstates/TextTitleSelectState.hpp" +#include "appstates/MainMenuState.hpp" +#include "colors.hpp" +#include "config.hpp" +#include "input.hpp" +#include "sdl.hpp" +#include + +namespace +{ + // All of these states share this same target. + constexpr std::string_view SECONDARY_TARGET = "SecondaryTarget"; +} // namespace + +TextTitleSelectState::TextTitleSelectState(data::User *user) + : TitleSelectCommon(), m_user(user), m_titleSelectMenu(32, 8, 1000, 20, 555), + m_renderTarget(sdl::TextureManager::createLoadTexture(SECONDARY_TARGET, 1080, 555, SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET)) +{ + TextTitleSelectState::refresh(); +} + +void TextTitleSelectState::update(void) +{ + m_titleSelectMenu.update(AppState::hasFocus()); + + if (input::buttonPressed(HidNpadButton_Y)) + { + config::addRemoveFavorite(m_user->getApplicationIDAt(m_titleSelectMenu.getSelected())); + MainMenuState::refreshViewStates(); + } + else if (input::buttonPressed(HidNpadButton_B)) + { + AppState::deactivate(); + } +} + +void TextTitleSelectState::render(void) +{ + m_renderTarget->clear(colors::TRANSPARENT); + m_titleSelectMenu.render(m_renderTarget->get(), AppState::hasFocus()); + TitleSelectCommon::renderControlGuide(); + m_renderTarget->render(NULL, 201, 91); +} + +void TextTitleSelectState::refresh(void) +{ + m_titleSelectMenu.reset(); + for (size_t i = 0; i < m_user->getTotalDataEntries(); i++) + { + std::string option; + uint64_t applicationID = m_user->getApplicationIDAt(i); + const char *title = data::getTitleInfoByID(applicationID)->getTitle(); + if (config::isFavorite(applicationID)) + { + option = std::string("^\uE017^ ") + title; + } + else + { + option = title; + } + m_titleSelectMenu.addOption(option.c_str()); + } +} diff --git a/source/appstates/TitleSelectCommon.cpp b/source/appstates/TitleSelectCommon.cpp new file mode 100644 index 0000000..6a33f74 --- /dev/null +++ b/source/appstates/TitleSelectCommon.cpp @@ -0,0 +1,26 @@ +#include "appstates/TitleSelectCommon.hpp" +#include "colors.hpp" +#include "sdl.hpp" +#include "strings.hpp" + +TitleSelectCommon::TitleSelectCommon(void) +{ + if (m_titleControlsX == 0) + { + m_titleControlsX = 1220 - sdl::text::getWidth(22, strings::getByName(strings::names::CONTROL_GUIDES, 1)); + } +} + +void TitleSelectCommon::renderControlGuide(void) +{ + if (AppState::hasFocus()) + { + sdl::text::render(NULL, + m_titleControlsX, + 673, + 22, + sdl::text::NO_TEXT_WRAP, + colors::WHITE, + strings::getByName(strings::names::CONTROL_GUIDES, 1)); + } +} diff --git a/source/appstates/TitleSelectState.cpp b/source/appstates/TitleSelectState.cpp new file mode 100644 index 0000000..a03980b --- /dev/null +++ b/source/appstates/TitleSelectState.cpp @@ -0,0 +1,75 @@ +#include "appstates/TitleSelectState.hpp" +#include "JKSV.hpp" +#include "appstates/BackupMenuState.hpp" +#include "appstates/MainMenuState.hpp" +#include "colors.hpp" +#include "config.hpp" +#include "fs/fs.hpp" +#include "fslib.hpp" +#include "input.hpp" +#include "logger.hpp" +#include "sdl.hpp" +#include "strings.hpp" +#include + +namespace +{ + // All of these states share the same render target. + constexpr std::string_view SECONDARY_TARGET = "SecondaryTarget"; +} // namespace + +TitleSelectState::TitleSelectState(data::User *user) + : TitleSelectCommon(), m_user(user), + m_renderTarget(sdl::TextureManager::createLoadTexture(SECONDARY_TARGET, 1080, 555, SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET)), + m_titleView(m_user) {}; + +void TitleSelectState::update(void) +{ + m_titleView.update(AppState::hasFocus()); + + if (input::buttonPressed(HidNpadButton_A)) + { + // Get data needed to mount save. + uint64_t applicationID = m_user->getApplicationIDAt(m_titleView.getSelected()); + FsSaveDataInfo *saveInfo = m_user->getSaveInfoByID(applicationID); + data::TitleInfo *titleInfo = data::getTitleInfoByID(applicationID); + + // Path to output to. + fslib::Path targetPath = config::getWorkingDirectory() / titleInfo->getPathSafeTitle(); + + if ((fslib::directoryExists(targetPath) || fslib::createDirectory(targetPath)) && + fslib::openSaveFileSystemWithSaveDataInfo(fs::DEFAULT_SAVE_MOUNT, *saveInfo)) + { + JKSV::pushState(std::make_shared(m_user, titleInfo, static_cast(saveInfo->save_data_type))); + } + else + { + logger::log("%s", fslib::getErrorString()); + } + } + else if (input::buttonPressed(HidNpadButton_B)) + { + // This will reset all the tiles so they're 128x128. + m_titleView.reset(); + AppState::deactivate(); + } + else if (input::buttonPressed(HidNpadButton_Y)) + { + config::addRemoveFavorite(m_user->getApplicationIDAt(m_titleView.getSelected())); + // MainMenuState has all the Users and views, so have it refresh. + MainMenuState::refreshViewStates(); + } +} + +void TitleSelectState::render(void) +{ + m_renderTarget->clear(colors::TRANSPARENT); + m_titleView.render(m_renderTarget->get(), AppState::hasFocus()); + TitleSelectCommon::renderControlGuide(); + m_renderTarget->render(NULL, 201, 91); +} + +void TitleSelectState::refresh(void) +{ + m_titleView.refresh(); +} diff --git a/source/appstates/UserOptionState.cpp b/source/appstates/UserOptionState.cpp new file mode 100644 index 0000000..dc18981 --- /dev/null +++ b/source/appstates/UserOptionState.cpp @@ -0,0 +1,176 @@ +#include "appstates/UserOptionState.hpp" +#include "JKSV.hpp" +#include "appstates/ProgressState.hpp" +#include "appstates/SaveCreateState.hpp" +#include "appstates/TaskState.hpp" +#include "config.hpp" +#include "data/data.hpp" +#include "fs/fs.hpp" +#include "fslib.hpp" +#include "input.hpp" +#include "logger.hpp" +#include "stringUtil.hpp" +#include "strings.hpp" +#include "system/system.hpp" +#include "ui/PopMessageManager.hpp" + +// Declarations here. Defintions after class. +// Backs up all save data for the target user. +static void backupAllForUser(sys::ProgressTask *task, data::User *targetUser); +// // Creates all save data for the current user. +// static void createAllSaveDataForUser(sys::Task *task, data::User *targetUser); +// // Deletes all save data from the system for the target user. +// static void deleteAllSaveDataForUser(sys::Task *task, data::User *targetUser); + +UserOptionState::UserOptionState(data::User *user, TitleSelectCommon *titleSelect) + : m_user(user), m_titleSelect(titleSelect), m_userOptionMenu(8, 8, 460, 22, 720) +{ + // Check if panel needs to be created. It's shared by all instances. + if (!m_menuPanel) + { + m_menuPanel = std::make_unique(480, ui::SlideOutPanel::Side::Right); + } + + int currentStringIndex = 0; + const char *currentString = nullptr; + while ((currentString = strings::getByName(strings::names::USER_OPTIONS, currentStringIndex++)) != nullptr) + { + m_userOptionMenu.addOption(stringutil::getFormattedString(currentString, m_user->getNickname())); + } +} + +void UserOptionState::update(void) +{ + m_menuPanel->update(AppState::hasFocus()); + + if (input::buttonPressed(HidNpadButton_A) && m_user->getAccountSaveType() != FsSaveDataType_System) + { + switch (m_userOptionMenu.getSelected()) + { + case 0: + { + JKSV::pushState(std::make_shared(backupAllForUser, m_user)); + } + break; + + case 1: + { + JKSV::pushState(std::make_shared(m_user, m_titleSelect)); + } + break; + } + } + else if (input::buttonPressed(HidNpadButton_B)) + { + m_menuPanel->close(); + } + else if (m_menuPanel->isClosed()) + { + AppState::deactivate(); + m_menuPanel->reset(); + } + + m_userOptionMenu.update(AppState::hasFocus()); +} + +void UserOptionState::render(void) +{ + // Render target user's title selection screen. + m_titleSelect->render(); + + // Render panel. + m_menuPanel->clearTarget(); + m_userOptionMenu.render(m_menuPanel->get(), AppState::hasFocus()); + m_menuPanel->render(NULL, AppState::hasFocus()); +} + +static void backupAllForUser(sys::ProgressTask *task, data::User *targetUser) +{ + for (size_t i = 0; i < targetUser->getTotalDataEntries(); i++) + { + // This should be safe like this.... + FsSaveDataInfo *currentSaveInfo = targetUser->getSaveInfoAt(i); + data::TitleInfo *currentTitle = data::getTitleInfoByID(currentSaveInfo->application_id); + + if (!currentSaveInfo || !currentTitle) + { + logger::log("One of these is nullptr?"); + continue; + } + + // Try to create target game folder. + fslib::Path gameFolder = config::getWorkingDirectory() / currentTitle->getPathSafeTitle(); + if (!fslib::directoryExists(gameFolder) && !fslib::createDirectory(gameFolder)) + { + logger::log("Error creating target game folder: %s", fslib::getErrorString()); + continue; + } + + // Try to mount save data. + bool saveMounted = fslib::openSaveFileSystemWithSaveDataInfo(fs::DEFAULT_SAVE_MOUNT, *targetUser->getSaveInfoAt(i)); + + // Check to make sure the save actually has data to avoid blanks. + { + fslib::Directory saveCheck(fs::DEFAULT_SAVE_PATH); + if (saveMounted && saveCheck.getCount() <= 0) + { + ui::PopMessageManager::pushMessage(ui::PopMessageManager::DEFAULT_MESSAGE_TICKS, + strings::getByName(strings::names::POP_MESSAGES, 0)); + fslib::closeFileSystem(fs::DEFAULT_SAVE_MOUNT); + continue; + } + } + + if (currentTitle && saveMounted && config::getByKey(config::keys::EXPORT_TO_ZIP)) + { + fslib::Path targetPath = config::getWorkingDirectory() / currentTitle->getPathSafeTitle() / targetUser->getPathSafeNickname() + + " - " + stringutil::getDateString() + ".zip"; + + zipFile targetZip = zipOpen64(targetPath.cString(), APPEND_STATUS_CREATE); + if (!targetZip) + { + logger::log("Error creating zip: %s", fslib::getErrorString()); + continue; + } + fs::copyDirectoryToZip(fs::DEFAULT_SAVE_PATH, targetZip, task); + zipClose(targetZip, NULL); + } + else if (currentTitle && saveMounted) + { + fslib::Path targetPath = config::getWorkingDirectory() / currentTitle->getPathSafeTitle() / targetUser->getPathSafeNickname() + + " - " + stringutil::getDateString(); + + if (!fslib::createDirectory(targetPath)) + { + logger::log("Error creating backup directory: %s", fslib::getErrorString()); + continue; + } + fs::copyDirectory(fs::DEFAULT_SAVE_PATH, targetPath, 0, {}, task); + } + + if (saveMounted) + { + fslib::closeFileSystem(fs::DEFAULT_SAVE_MOUNT); + } + } + task->finished(); +} + +// static void createAllSaveDataForUser(sys::Task *task, data::User *targetUser) +// { +// // Get title info map. +// auto &titleInfoMap = data::getTitleInfoMap(); + +// // Iterate through it. +// for (auto &[applicationID, titleInfo] : titleInfoMap) +// { +// // Only continue if the info has save data for the type the account is. +// if (titleInfo.hasSaveDataType(targetUser->getAccountSaveType())) +// { +// } +// } +// } + +// static void deleteAllSaveDataForUser(sys::Task *task, data::User *targetUser) +// { +// } diff --git a/source/config.cpp b/source/config.cpp new file mode 100644 index 0000000..8b8d1f0 --- /dev/null +++ b/source/config.cpp @@ -0,0 +1,231 @@ +#include "config.hpp" +#include "JSON.hpp" +#include "logger.hpp" +#include "stringUtil.hpp" +#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"; + // Vector to preserve order now. + std::vector> s_configVector; + // 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(std::strtoull(json_object_get_string(arrayEntry), NULL, 16)); + } +} + +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::WORKING_DIRECTORY.data()) == 0) + { + s_workingDirectory = json_object_get_string(configValue); + } + else if (std::strcmp(keyName, config::keys::UI_ANIMATION_SCALE.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_configVector.push_back(std::make_pair(keyName, json_object_get_uint64(configValue))); + } + json_object_iter_next(&configIterator); + } +} + +void config::resetToDefault(void) +{ + s_workingDirectory = "sdmc:/JKSV"; + s_configVector.push_back(std::make_pair(config::keys::INCLUDE_DEVICE_SAVES.data(), 0)); + s_configVector.push_back(std::make_pair(config::keys::AUTO_BACKUP_ON_RESTORE.data(), 1)); + s_configVector.push_back(std::make_pair(config::keys::AUTO_NAME_BACKUPS.data(), 1)); + s_configVector.push_back(std::make_pair(config::keys::AUTO_UPLOAD.data(), 1)); + s_configVector.push_back(std::make_pair(config::keys::HOLD_FOR_DELETION.data(), 0)); + s_configVector.push_back(std::make_pair(config::keys::HOLD_FOR_RESTORATION.data(), 0)); + s_configVector.push_back(std::make_pair(config::keys::HOLD_FOR_OVERWRITE.data(), 0)); + s_configVector.push_back(std::make_pair(config::keys::ONLY_LIST_MOUNTABLE.data(), 0)); + s_configVector.push_back(std::make_pair(config::keys::LIST_ACCOUNT_SYS_SAVES.data(), 0)); + s_configVector.push_back(std::make_pair(config::keys::ALLOW_WRITING_TO_SYSTEM.data(), 0)); + s_configVector.push_back(std::make_pair(config::keys::EXPORT_TO_ZIP.data(), 0)); + s_configVector.push_back(std::make_pair(config::keys::ZIP_COMPRESSION_LEVEL.data(), 6)); + s_configVector.push_back(std::make_pair(config::keys::TITLE_SORT_TYPE.data(), 0)); + s_configVector.push_back(std::make_pair(config::keys::JKSM_TEXT_MODE.data(), 0)); + s_configVector.push_back(std::make_pair(config::keys::FORCE_ENGLISH.data(), 0)); + s_configVector.push_back(std::make_pair(config::keys::ENABLE_TRASH_BIN.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::WORKING_DIRECTORY.data(), workingDirectory); + + // Loop through map and add it. + for (auto &[key, value] : s_configVector) + { + 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::UI_ANIMATION_SCALE.data(), scaling); + + // Favorites + json_object *favoritesArray = json_object_new_array(); + for (uint64_t &titleID : s_favorites) + { + // Need to do it like this or json-c does decimal instead of hex. + json_object *newFavorite = json_object_new_string(stringutil::getFormattedString("%016lX", titleID).c_str()); + 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_string(stringutil::getFormattedString("%016lX", titleID).c_str()); + 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) +{ + auto findKey = + std::find_if(s_configVector.begin(), s_configVector.end(), [key](const auto &configPair) { return key == configPair.first; }); + if (findKey == s_configVector.end()) + { + return 0; + } + return findKey->second; +} + +uint8_t config::getByIndex(int index) +{ + if (index < 0 || index >= static_cast(s_configVector.size())) + { + return 0; + } + return s_configVector.at(index).second; +} + +fslib::Path config::getWorkingDirectory(void) +{ + return s_workingDirectory; +} + +double config::getAnimationScaling(void) +{ + return s_uiAnimationScaling; +} + +void config::addRemoveFavorite(uint64_t applicationID) +{ + auto findTitle = std::find(s_favorites.begin(), s_favorites.end(), applicationID); + if (findTitle == s_favorites.end()) + { + s_favorites.push_back(applicationID); + } + else + { + s_favorites.erase(findTitle); + } +} + +bool config::isFavorite(uint64_t applicationID) +{ + if (std::find(s_favorites.begin(), s_favorites.end(), applicationID) == s_favorites.end()) + { + return false; + } + return true; +} + +void config::addRemoveBlacklist(uint64_t applicationID) +{ + auto findTitle = std::find(s_blacklist.begin(), s_blacklist.end(), applicationID); + if (findTitle == s_blacklist.end()) + { + s_blacklist.push_back(applicationID); + } + else + { + s_blacklist.erase(findTitle); + } +} + +bool config::isBlacklisted(uint64_t applicationID) +{ + if (std::find(s_blacklist.begin(), s_blacklist.end(), applicationID) == s_blacklist.end()) + { + return false; + } + return true; +} diff --git a/source/data/TitleInfo.cpp b/source/data/TitleInfo.cpp new file mode 100644 index 0000000..91d36a5 --- /dev/null +++ b/source/data/TitleInfo.cpp @@ -0,0 +1,310 @@ +#include "data/TitleInfo.hpp" +#include "colors.hpp" +#include "logger.hpp" +#include "stringUtil.hpp" +#include + +data::TitleInfo::TitleInfo(uint64_t applicationID) : m_applicationID(applicationID) +{ + // Used to calculate icon size. + uint64_t nsAppControlSize = 0; + // Actual control data. + NsApplicationControlData nsControlData; + // 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)); + + // Sprintf title ids to language entries for safety. + snprintf(m_nacp.lang[SetLanguage_ENUS].name, 0x200, "%016lX", applicationID); + snprintf(m_pathSafeTitle, 0x200, "%016lX", applicationID); + + // Create a place holder icon. + int textX = 128 - (sdl::text::getWidth(48, applicationIDHex.c_str()) / 2); + m_icon = sdl::TextureManager::createLoadTexture(applicationIDHex, 256, 256, SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET); + m_icon->clear(colors::DIALOG_BOX); + sdl::text::render(m_icon->get(), textX, 104, 48, 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)); + // Get a path safe version of the title. + if (!stringutil::sanitizeStringForPath(languageEntry->name, m_pathSafeTitle, 0x200)) + { + std::sprintf(m_pathSafeTitle, "%016lX", applicationID); + } + // Load the icon. + m_icon = sdl::TextureManager::createLoadTexture(languageEntry->name, nsControlData.icon, nsAppControlSize - sizeof(NacpStruct)); + } +} + +uint64_t data::TitleInfo::getApplicationID(void) const +{ + return m_applicationID; +} + +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::getPathSafeTitle(void) +{ + return m_pathSafeTitle; +} + +const char *data::TitleInfo::getPublisher(void) +{ + NacpLanguageEntry *Entry = nullptr; + if (R_FAILED(nacpGetLanguageEntry(&m_nacp, &Entry))) + { + return nullptr; + } + return Entry->author; +} + +uint64_t data::TitleInfo::getSaveDataOwnerID(void) const +{ + return m_nacp.save_data_owner_id; +} + +int64_t data::TitleInfo::getSaveDataSize(FsSaveDataType saveType) const +{ + switch (saveType) + { + case FsSaveDataType_Account: + { + return m_nacp.user_account_save_data_size; + } + break; + + case FsSaveDataType_Bcat: + { + return m_nacp.bcat_delivery_cache_storage_size; + } + break; + + case FsSaveDataType_Device: + { + return m_nacp.device_save_data_size; + } + break; + + case FsSaveDataType_Temporary: + { + return m_nacp.temporary_storage_size; + } + break; + + case FsSaveDataType_Cache: + { + return m_nacp.cache_storage_size; + } + break; + + default: + { + return 0; + } + break; + } + return 0; +} + +int64_t data::TitleInfo::getSaveDataSizeMax(FsSaveDataType saveType) const +{ + switch (saveType) + { + case FsSaveDataType_Account: + { + return m_nacp.user_account_save_data_size_max > m_nacp.user_account_save_data_size ? m_nacp.user_account_save_data_size_max + : m_nacp.user_account_save_data_size; + } + break; + + case FsSaveDataType_Bcat: + { + return m_nacp.bcat_delivery_cache_storage_size; + } + break; + + case FsSaveDataType_Device: + { + return m_nacp.device_save_data_size_max > m_nacp.device_save_data_size ? m_nacp.device_save_data_size_max + : m_nacp.device_save_data_size; + } + break; + + case FsSaveDataType_Temporary: + { + return m_nacp.temporary_storage_size; + } + break; + + case FsSaveDataType_Cache: + { + return m_nacp.cache_storage_data_and_journal_size_max > m_nacp.cache_storage_size ? m_nacp.cache_storage_data_and_journal_size_max + : m_nacp.cache_storage_size; + } + break; + + default: + { + return 0; + } + break; + } + return 0; +} + +int64_t data::TitleInfo::getJournalSize(FsSaveDataType saveType) const +{ + switch (saveType) + { + case FsSaveDataType_Account: + { + return m_nacp.user_account_save_data_journal_size; + } + break; + + case FsSaveDataType_Bcat: + { + // I'm just assuming this is right... + return m_nacp.bcat_delivery_cache_storage_size; + } + break; + + case FsSaveDataType_Device: + { + return m_nacp.device_save_data_journal_size; + } + break; + + case FsSaveDataType_Temporary: + { + // Again, just assuming. + return m_nacp.temporary_storage_size; + } + break; + + case FsSaveDataType_Cache: + { + return m_nacp.cache_storage_journal_size; + } + break; + + default: + { + return 0; + } + break; + } + return 0; +} + +int64_t data::TitleInfo::getJournalSizeMax(FsSaveDataType saveType) const +{ + switch (saveType) + { + case FsSaveDataType_Account: + { + return m_nacp.user_account_save_data_journal_size_max > m_nacp.user_account_save_data_journal_size + ? m_nacp.user_account_save_data_journal_size_max + : m_nacp.user_account_save_data_journal_size; + } + break; + + case FsSaveDataType_Bcat: + { + return m_nacp.bcat_delivery_cache_storage_size; + } + break; + + case FsSaveDataType_Device: + { + return m_nacp.device_save_data_journal_size_max > m_nacp.device_save_data_journal_size ? m_nacp.device_save_data_journal_size_max + : m_nacp.device_save_data_journal_size; + } + break; + + case FsSaveDataType_Temporary: + { + return m_nacp.temporary_storage_size; + } + break; + + case FsSaveDataType_Cache: + { + return m_nacp.cache_storage_data_and_journal_size_max > m_nacp.cache_storage_journal_size + ? m_nacp.cache_storage_data_and_journal_size_max + : m_nacp.cache_storage_journal_size; + } + break; + + default: + { + return 0; + } + break; + } + return 0; +} + +bool data::TitleInfo::hasSaveDataType(FsSaveDataType saveType) +{ + switch (saveType) + { + case FsSaveDataType_Account: + { + return m_nacp.user_account_save_data_size > 0 || m_nacp.user_account_save_data_size_max > 0; + } + break; + + case FsSaveDataType_Bcat: + { + return m_nacp.bcat_delivery_cache_storage_size > 0; + } + break; + + case FsSaveDataType_Device: + { + return m_nacp.device_save_data_size > 0 || m_nacp.device_save_data_size_max > 0; + } + break; + + case FsSaveDataType_Cache: + { + return m_nacp.cache_storage_size > 0 || m_nacp.cache_storage_data_and_journal_size_max > 0; + } + break; + + default: + { + return false; + } + break; + } + return false; +} + +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..504da0f --- /dev/null +++ b/source/data/User.cpp @@ -0,0 +1,264 @@ +#include "data/User.hpp" +#include "colors.hpp" +#include "config.hpp" +#include "data/data.hpp" +#include "logger.hpp" +#include "sdl.hpp" +#include "stringUtil.hpp" +#include +#include + +namespace +{ + /// @brief Font size for rendering text to icons. + constexpr int ICON_FONT_SIZE = 50; +} // namespace + +// Function used to sort user data. +static bool sortUserData(const data::UserDataEntry &entryA, const data::UserDataEntry &entryB) +{ + auto &[applicationIDA, dataA] = entryA; + auto &[applicationIDB, dataB] = entryB; + auto &[saveInfoA, playStatsA] = dataA; + auto &[saveInfoB, playStatsB] = dataB; + + // Favorites over all. + if (config::isFavorite(applicationIDA) != config::isFavorite(applicationIDB)) + { + return config::isFavorite(applicationIDA); + } + + data::TitleInfo *titleInfoA = data::getTitleInfoByID(applicationIDA); + data::TitleInfo *titleInfoB = data::getTitleInfoByID(applicationIDB); + switch (config::getByKey(config::keys::TITLE_SORT_TYPE)) + { + // Alpha + case 0: + { + // Get titles + const char *titleA = titleInfoA->getTitle(); + const char *titleB = titleInfoB->getTitle(); + + // Get the shortest of the two. + size_t titleALength = std::char_traits::length(titleA); + size_t titleBLength = std::char_traits::length(titleB); + size_t shortestTitle = titleALength < titleBLength ? titleALength : titleBLength; + // Loop and compare codepoints. + for (size_t i = 0, j = 0; i < shortestTitle;) + { + // Decode UTF-8 + uint32_t codepointA = 0; + uint32_t codepointB = 0; + ssize_t unitCountA = decode_utf8(&codepointA, reinterpret_cast(&titleA[i])); + ssize_t unitCountB = decode_utf8(&codepointB, reinterpret_cast(&titleB[j])); + + // Lower so case doesn't screw with it. + int charA = std::tolower(codepointA); + int charB = std::tolower(codepointB); + if (charA != charB) + { + return charA < charB; + } + + i += unitCountA; + j += unitCountB; + } + } + break; + + // Most played. + case 1: + { + return playStatsA.playtime > playStatsB.playtime; + } + break; + + // Last played. + case 2: + { + return playStatsA.last_timestamp_user > playStatsB.last_timestamp_user; + } + break; + } + return false; +} + +data::User::User(AccountUid accountID, FsSaveDataType saveType) : m_accountID(accountID), m_saveType(saveType) +{ + 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 pathSafeNickname, std::string_view iconPath, FsSaveDataType saveType) + : m_accountID(accountID), m_saveType(saveType), m_icon(sdl::TextureManager::createLoadTexture(pathSafeNickname, iconPath.data())) +{ + std::memcpy(m_pathSafeNickname, pathSafeNickname.data(), pathSafeNickname.length()); +} + +void data::User::addData(const FsSaveDataInfo &saveInfo, const PdmPlayStatistics &playStats) +{ + uint64_t applicationID = saveInfo.application_id == 0 ? saveInfo.system_save_data_id : saveInfo.application_id; + + m_userData.push_back(std::make_pair(applicationID, std::make_pair(saveInfo, playStats))); +} + +void data::User::sortData(void) +{ + std::sort(m_userData.begin(), m_userData.end(), sortUserData); +} + +AccountUid data::User::getAccountID(void) const +{ + return m_accountID; +} + +FsSaveDataType data::User::getAccountSaveType(void) const +{ + return m_saveType; +} + +const char *data::User::getNickname(void) const +{ + return m_nickname; +} + +const char *data::User::getPathSafeNickname(void) const +{ + return m_pathSafeNickname; +} + +size_t data::User::getTotalDataEntries(void) const +{ + return m_userData.size(); +} + +uint64_t data::User::getApplicationIDAt(int index) const +{ + if (index < 0 || index >= static_cast(m_userData.size())) + { + return 0; + } + return m_userData.at(index).first; +} + +FsSaveDataInfo *data::User::getSaveInfoAt(int index) +{ + if (index < 0 || index >= static_cast(m_userData.size())) + { + return nullptr; + } + return &m_userData.at(index).second.first; +} + +PdmPlayStatistics *data::User::getPlayStatsAt(int index) +{ + if (index < 0 || index >= static_cast(m_userData.size())) + { + return nullptr; + } + return &m_userData.at(index).second.second; +} + +FsSaveDataInfo *data::User::getSaveInfoByID(uint64_t applicationID) +{ + auto findTitle = std::find_if(m_userData.begin(), m_userData.end(), [applicationID](data::UserDataEntry &entry) { + return entry.first == applicationID; + }); + + if (findTitle == m_userData.end()) + { + return nullptr; + } + return &findTitle->second.first; +} + +PdmPlayStatistics *data::User::getPlayStatsByID(uint64_t applicationID) +{ + auto findTitle = std::find_if(m_userData.begin(), m_userData.end(), [applicationID](data::UserDataEntry &entry) { + return entry.first == applicationID; + }); + + if (findTitle == m_userData.end()) + { + return nullptr; + } + return &findTitle->second.second; +} + +SDL_Texture *data::User::getIcon(void) +{ + return m_icon->get(); +} + +sdl::SharedTexture data::User::getSharedIcon(void) +{ + return m_icon; +} + +void data::User::loadAccount(AccountProfile &profile, AccountProfileBase &profileBase) +{ + // Try to load icon. + uint32_t iconSize = 0; + Result accError = accountProfileGetImageSize(&profile, &iconSize); + if (R_FAILED(accError)) + { + logger::log("Error getting user icon size: 0x%X.", accError); + User::createAccount(); + return; + } + + std::unique_ptr iconBuffer(new unsigned char[iconSize]); + accError = accountProfileLoadImage(&profile, iconBuffer.get(), iconSize, &iconSize); + if (R_FAILED(accError)) + { + logger::log("Error loading user icon: 0x%08X.", accError); + 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("Acc_%08X", m_accountID.uid[0] & 0xFFFFFFFF); + + // Create icon + int textX = 128 - (sdl::text::getWidth(ICON_FONT_SIZE, 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, + 128 - (ICON_FONT_SIZE / 2), + ICON_FONT_SIZE, + 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/data/data.cpp b/source/data/data.cpp new file mode 100644 index 0000000..84f3299 --- /dev/null +++ b/source/data/data.cpp @@ -0,0 +1,208 @@ +#include "data/data.hpp" +#include "config.hpp" +#include "fs/fs.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 SAVE_DATA_SPACE_ORDER = {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 totalAccounts = 0; + AccountUid accountIDs[8]; + Result accError = accountListAllUsers(accountIDs, 8, &totalAccounts); + if (R_FAILED(accError)) + { + logger::log("Error getting user list: 0x%X.", accError); + return false; + } + + // Loop through and load all users found. + for (int i = 0; i < totalAccounts; i++) + { + data::User newUser(accountIDs[i], FsSaveDataType_Account); + s_userVector.push_back(std::make_pair(accountIDs[i], newUser)); + } + + // Need this for save data of deleted users. It does happen. This is where they are inserted. + // size_t userInsertPosition = s_userVector.size() - 1; + + // "System" users. + constexpr AccountUid deviceID = {FsSaveDataType_Device}; + constexpr AccountUid bcatID = {FsSaveDataType_Bcat}; + constexpr AccountUid cacheID = {FsSaveDataType_Cache}; + constexpr AccountUid systemID = {FsSaveDataType_System}; + + s_userVector.push_back(std::make_pair(deviceID, data::User(deviceID, "Device", "romfs:/Textures/SystemSaves.png", FsSaveDataType_Device))); + s_userVector.push_back(std::make_pair(bcatID, data::User(bcatID, "BCAT", "romfs:/Textures/BCAT.png", FsSaveDataType_Bcat))); + s_userVector.push_back(std::make_pair(cacheID, data::User(cacheID, "Cache", "romfs:/Textures/Cache.png", FsSaveDataType_Cache))); + s_userVector.push_back(std::make_pair(systemID, data::User(systemID, "System", "romfs:/Textures/SystemSaves.png", FsSaveDataType_System))); + + NsApplicationRecord currentRecord = {0}; + int entryCount = 0, entryOffset = 0; + while (R_SUCCEEDED(nsListApplicationRecord(¤tRecord, 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++) + { + fslib::SaveInfoReader saveInfoReader(SAVE_DATA_SPACE_ORDER[i]); + if (!saveInfoReader.isOpen()) + { + logger::log(fslib::getErrorString()); + continue; + } + + while (saveInfoReader.read()) + { + FsSaveDataInfo &saveInfo = saveInfoReader.get(); + + // This will filter out the account system saves unless the config is set to show them. + if (!config::getByKey(config::keys::LIST_ACCOUNT_SYS_SAVES) && saveInfo.save_data_type == FsSaveDataType_System && + saveInfo.uid != 0) + { + continue; + } + + // Since JKSV uses fake users to support system type saves. + AccountUid accountID; + switch (saveInfo.save_data_type) + { + case FsSaveDataType_Bcat: + { + accountID = {FsSaveDataType_Bcat}; + } + break; + + case FsSaveDataType_Device: + { + accountID = {FsSaveDataType_Device}; + } + break; + + case FsSaveDataType_Cache: + { + accountID = {FsSaveDataType_Cache}; + } + break; + + default: + { + accountID = saveInfo.uid; + } + break; + } + + if (config::getByKey(config::keys::ONLY_LIST_MOUNTABLE) && + !fslib::openSaveFileSystemWithSaveDataInfo(fs::DEFAULT_SAVE_MOUNT, saveInfo)) + { + // Continue the loop since mounting failed. + continue; + } + fslib::closeFileSystem(fs::DEFAULT_SAVE_MOUNT); + + // Find the user with the ID. + auto findUser = std::find_if(s_userVector.begin(), s_userVector.end(), [accountID](const UserIDPair &userPair) { + return accountID == userPair.second.getAccountID(); + }); + + if (findUser == s_userVector.end()) + { + // To do: Handle this like old JKSV did. + continue; + } + + // This is for system save data since 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; + + 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); + } + // Push it to user. + findUser->second.addData(saveInfo, playStats); + } + } + + // Sort data for users. + for (auto &[accountID, user] : s_userVector) + { + user.sortData(); + } + + // Wew + return true; +} + +void data::getUsers(std::vector &vectorOut) +{ + vectorOut.clear(); + for (auto &[accountID, userData] : s_userVector) + { + vectorOut.push_back(&userData); + } +} + +data::TitleInfo *data::getTitleInfoByID(uint64_t applicationID) +{ + if (s_titleInfoMap.find(applicationID) == s_titleInfoMap.end()) + { + return nullptr; + } + return &s_titleInfoMap.at(applicationID); +} + +std::unordered_map &data::getTitleInfoMap(void) +{ + return s_titleInfoMap; +} + +void data::getTitleInfoByType(FsSaveDataType saveType, std::vector &vectorOut) +{ + // Clear vector JIC + vectorOut.clear(); + + // Loop and push pointers + for (auto &[applicationID, titleInfo] : s_titleInfoMap) + { + if (titleInfo.hasSaveDataType(saveType)) + { + vectorOut.push_back(&titleInfo); + } + } +} diff --git a/source/fs/createSaveData.cpp b/source/fs/createSaveData.cpp new file mode 100644 index 0000000..14610a3 --- /dev/null +++ b/source/fs/createSaveData.cpp @@ -0,0 +1,34 @@ +#include "fs/createSaveData.hpp" +#include "logger.hpp" + +bool fs::createSaveDataFor(data::User *targetUser, data::TitleInfo *titleInfo) +{ + // Attributes. + FsSaveDataAttribute saveAttributes = {.application_id = titleInfo->getApplicationID(), + .uid = targetUser->getAccountSaveType() == FsSaveDataType_Account ? targetUser->getAccountID() + : data::BLANK_ACCOUNT_ID, + .system_save_data_id = 0, + .save_data_type = targetUser->getAccountSaveType(), + .save_data_rank = FsSaveDataRank_Primary, + .save_data_index = 0}; + + FsSaveDataCreationInfo saveCreation = { + .save_data_size = titleInfo->getSaveDataSize(targetUser->getAccountSaveType()), + .journal_size = titleInfo->getJournalSize(targetUser->getAccountSaveType()), + .available_size = 0x4000, + .owner_id = targetUser->getAccountSaveType() == FsSaveDataType_Bcat ? 0x010000000000000C : titleInfo->getSaveDataOwnerID(), + .flags = 0, + .save_data_space_id = FsSaveDataSpaceId_User}; + + // Save meta + FsSaveDataMetaInfo saveMeta = {.size = 0x40060, .type = FsSaveDataMetaType_Thumbnail}; + + Result fsError = fsCreateSaveDataFileSystem(&saveAttributes, &saveCreation, &saveMeta); + if (R_FAILED(fsError)) + { + logger::log("Error creating save data for %016llX: 0x%X.", titleInfo->getApplicationID(), fsError); + return false; + } + + return true; +} diff --git a/source/fs/io.cpp b/source/fs/io.cpp new file mode 100644 index 0000000..234f6e7 --- /dev/null +++ b/source/fs/io.cpp @@ -0,0 +1,168 @@ +#include "fs/io.hpp" +#include "logger.hpp" +#include "strings.hpp" +#include +#include +#include +#include + +namespace +{ + // Size of buffer shared between threads. + // constexpr size_t FILE_BUFFER_SIZE = 0x600000; + // This one is just for testing something. + constexpr size_t FILE_BUFFER_SIZE = 0x80000; +} // namespace + +// Struct threads shared to read and write files. +typedef struct +{ + // Mutex to lock buffer. + std::mutex m_bufferLock; + // Conditional to wait on signals. + std::condition_variable m_bufferCondition; + // Bool to control signals. + bool m_bufferIsFull = false; + // Number of bytes read. + size_t m_readSize = 0; + // Shared (read) buffer. + std::unique_ptr m_readBuffer; +} FileTransferStruct; + +// This function Reads into the buffer. The other thread writes. +static void readThreadFunction(fslib::File &sourceFile, std::shared_ptr sharedData) +{ + int64_t fileSize = sourceFile.getSize(); + for (int64_t readCount = 0; readCount < fileSize;) + { + // Read data to shared buffer. + sharedData->m_readSize = sourceFile.read(sharedData->m_readBuffer.get(), FILE_BUFFER_SIZE); + // Update local read count + readCount += sharedData->m_readSize; + // Signal to other thread buffer is full. + sharedData->m_bufferIsFull = true; + sharedData->m_bufferCondition.notify_one(); + // Wait for other thread to signal buffer is empty. Lock is released immediately, but it works and that's what matters. + std::unique_lock m_bufferLock(sharedData->m_bufferLock); + sharedData->m_bufferCondition.wait(m_bufferLock, [&sharedData]() { return sharedData->m_bufferIsFull == false; }); + } +} + +void fs::copyFile(const fslib::Path &source, + const fslib::Path &destination, + uint64_t journalSize, + std::string_view commitDevice, + sys::ProgressTask *task) +{ + fslib::File sourceFile(source, FsOpenMode_Read); + fslib::File destinationFile(destination, FsOpenMode_Create | FsOpenMode_Write, sourceFile.getSize()); + if (!sourceFile.isOpen() || !destinationFile.isOpen()) + { + logger::log("Error opening one of the files: %s", fslib::getErrorString()); + return; + } + + // Set status if task pointer was passed. + if (task) + { + task->setStatus(strings::getByName(strings::names::COPYING_FILES, 0), source.cString()); + } + + // Shared struct both threads use + std::shared_ptr sharedData(new FileTransferStruct); + sharedData->m_readBuffer = std::make_unique(FILE_BUFFER_SIZE); + + // To do: Static thread or pool to avoid reallocating thread. + std::thread readThread(readThreadFunction, std::ref(sourceFile), sharedData); + + // This thread has a local buffer so the read thread can continue while this one writes. + std::unique_ptr localBuffer(new unsigned char[FILE_BUFFER_SIZE]); + + // Get file size for loop and set goal. + int64_t fileSize = sourceFile.getSize(); + if (task) + { + task->reset(static_cast(fileSize)); + } + + for (int64_t writeCount = 0, readCount = 0, journalCount = 0; writeCount < fileSize;) + { + { + // Wait for lock/signal. + std::unique_lock m_bufferLock(sharedData->m_bufferLock); + sharedData->m_bufferCondition.wait(m_bufferLock, [&sharedData]() { return sharedData->m_bufferIsFull; }); + + // Record read count. + readCount = sharedData->m_readSize; + + // Copy shared to local. + std::memcpy(localBuffer.get(), sharedData->m_readBuffer.get(), readCount); + + // Signal buffer was copied and release mutex. + sharedData->m_bufferIsFull = false; + sharedData->m_bufferCondition.notify_one(); + } + + // Journaling size check. Breathing room is given. + if (journalSize != 0 && (journalCount + readCount) >= static_cast(journalSize) - 0x100000) + { + // Reset journal count. + journalCount = 0; + // Close destination file, commit. + destinationFile.close(); + fslib::commitDataToFileSystem(commitDevice); + // Reopen and seek to previous position since we created it with a size earlier. + destinationFile.open(destination, FsOpenMode_Write); + destinationFile.seek(writeCount, destinationFile.beginning); + } + // Write to destination + destinationFile.write(localBuffer.get(), readCount); + // Update write and journal count. + writeCount += readCount; + journalCount += readCount; + // Update task if passed. + if (task) + { + task->updateCurrent(static_cast(writeCount)); + } + } + // Wait for read thread and free it. + readThread.join(); +} + +void fs::copyDirectory(const fslib::Path &source, + const fslib::Path &destination, + uint64_t journalSize, + std::string_view commitDevice, + sys::ProgressTask *task) +{ + fslib::Directory sourceDir(source); + if (!sourceDir.isOpen()) + { + logger::log("Error opening directory for reading: %s", fslib::getErrorString()); + return; + } + + for (int64_t i = 0; i < sourceDir.getCount(); i++) + { + if (sourceDir.isDirectory(i)) + { + fslib::Path newSource = source / sourceDir[i]; + fslib::Path newDestination = destination / sourceDir[i]; + // Try to create new destination folder and continue loop on failure. + if (!fslib::directoryExists(newDestination) && !fslib::createDirectory(newDestination)) + { + logger::log("Error creating new destination directory: %s", fslib::getErrorString()); + continue; + } + + fs::copyDirectory(newSource, newDestination, journalSize, commitDevice, task); + } + else + { + fslib::Path fullSource = source / sourceDir[i]; + fslib::Path fullDestination = destination / sourceDir[i]; + fs::copyFile(fullSource, fullDestination, journalSize, commitDevice, task); + } + } +} diff --git a/source/fs/zip.cpp b/source/fs/zip.cpp new file mode 100644 index 0000000..fa60cc4 --- /dev/null +++ b/source/fs/zip.cpp @@ -0,0 +1,295 @@ +#include "fs/zip.hpp" +#include "config.hpp" +#include "logger.hpp" +#include "strings.hpp" +#include +#include +#include +#include +#include +#include + +namespace +{ + // Size used for Zipping files. + constexpr size_t ZIP_BUFFER_SIZE = 0x100000; + // Size used for unzipping. + constexpr size_t UNZIP_BUFFER_SIZE = 0x600000; +} // namespace + +// Shared struct for Zip/File IO +typedef struct +{ + // Mutex and condition for buffer. + std::mutex m_bufferLock; + std::condition_variable m_bufferCondition; + bool m_bufferIsFull = false; + // Number of bytes read from file. + ssize_t m_readCount = 0; + // Shared/reading buffer. + std::unique_ptr m_sharedBuffer; +} ZipIOStruct; + +// Function for reading files for Zipping. +static void zipReadThreadFunction(fslib::File &source, std::shared_ptr sharedData) +{ + int64_t fileSize = source.getSize(); + for (int64_t readCount = 0; readCount < fileSize;) + { + // Read into shared buffer. + sharedData->m_readCount = source.read(sharedData->m_sharedBuffer.get(), ZIP_BUFFER_SIZE); + // Update read count + readCount += sharedData->m_readCount; + // Signal other thread buffer is ready to go. + sharedData->m_bufferIsFull = true; + sharedData->m_bufferCondition.notify_one(); + // Wait for other thread to release lock on buffer so this thread can read again. + std::unique_lock m_bufferLock(sharedData->m_bufferLock); + sharedData->m_bufferCondition.wait(m_bufferLock, [&sharedData]() { return sharedData->m_bufferIsFull == false; }); + } +} + +// Function for reading data from Zip to buffer. +static void unzipReadThreadFunction(unzFile source, int64_t fileSize, std::shared_ptr sharedData) +{ + for (int64_t readCount = 0; readCount < fileSize;) + { + // Read from zip file. + sharedData->m_readCount = unzReadCurrentFile(source, sharedData->m_sharedBuffer.get(), UNZIP_BUFFER_SIZE); + + readCount += sharedData->m_readCount; + + sharedData->m_bufferIsFull = true; + sharedData->m_bufferCondition.notify_one(); + + std::unique_lock m_bufferLock(sharedData->m_bufferLock); + sharedData->m_bufferCondition.wait(m_bufferLock, [&sharedData]() { return sharedData->m_bufferIsFull == false; }); + } +} + +void fs::copyDirectoryToZip(const fslib::Path &source, zipFile destination, sys::ProgressTask *task) +{ + fslib::Directory sourceDir(source); + if (!sourceDir.isOpen()) + { + logger::log("Error opening source directory: %s", fslib::getErrorString()); + return; + } + + for (int64_t i = 0; i < sourceDir.getCount(); i++) + { + if (sourceDir.isDirectory(i)) + { + fslib::Path newSource = source / sourceDir[i]; + fs::copyDirectoryToZip(newSource, destination, task); + } + else + { + // Open source file. + fslib::Path fullSource = source / sourceDir[i]; + fslib::File sourceFile(fullSource, FsOpenMode_Read); + if (!sourceFile.isOpen()) + { + logger::log("Error zipping file: %s", fslib::getErrorString()); + continue; + } + + // Date for file(s) + std::time_t timer; + std::time(&timer); + std::tm *localTime = std::localtime(&timer); + zip_fileinfo FileInfo = {.tmz_date = {.tm_sec = localTime->tm_sec, + .tm_min = localTime->tm_min, + .tm_hour = localTime->tm_hour, + .tm_mday = localTime->tm_mday, + .tm_mon = localTime->tm_mon, + .tm_year = localTime->tm_year + 1900}, + .dosDate = 0, + .internal_fa = 0, + .external_fa = 0}; + + // Create new file in zip + const char *zipNameBegin = std::strchr(fullSource.getPath(), '/') + 1; + int zipError = zipOpenNewFileInZip64(destination, + zipNameBegin, + &FileInfo, + NULL, + 0, + NULL, + 0, + NULL, + Z_DEFLATED, + config::getByKey(config::keys::ZIP_COMPRESSION_LEVEL), + 1); + if (zipError != ZIP_OK) + { + logger::log("Error creating file in zip: %i.", zipError); + continue; + } + + // Shared data for thread. + std::shared_ptr sharedData(new ZipIOStruct); + sharedData->m_sharedBuffer = std::make_unique(ZIP_BUFFER_SIZE); + + // Local buffer for writing. + std::unique_ptr localBuffer(new unsigned char[ZIP_BUFFER_SIZE]); + + // Update task if passed. + if (task) + { + task->setStatus(strings::getByName(strings::names::COPYING_FILES, 1), fullSource.cString()); + task->reset(static_cast(sourceFile.getSize())); + } + + std::thread readThread(zipReadThreadFunction, std::ref(sourceFile), sharedData); + + int64_t fileSize = sourceFile.getSize(); + for (int64_t writeCount = 0, readCount = 0; writeCount < fileSize;) + { + { + // Wait for buffer signal + std::unique_lock m_bufferLock(sharedData->m_bufferLock); + sharedData->m_bufferCondition.wait(m_bufferLock, [&sharedData]() { return sharedData->m_bufferIsFull; }); + + // Save read count, copy shared to local. + readCount = sharedData->m_readCount; + std::memcpy(localBuffer.get(), sharedData->m_sharedBuffer.get(), readCount); + + // Signal copy was good and release lock. + sharedData->m_bufferIsFull = false; + sharedData->m_bufferCondition.notify_one(); + } + // Write + zipError = zipWriteInFileInZip(destination, localBuffer.get(), readCount); + if (zipError != ZIP_OK) + { + logger::log("Error writing data to zip: %i.", zipError); + } + // Update count and status + writeCount += readCount; + if (task) + { + task->updateCurrent(static_cast(writeCount)); + } + } + // Wait for thread + readThread.join(); + // Close file in zip + zipCloseFileInZip(destination); + } + } +} + +void fs::copyZipToDirectory(unzFile source, + const fslib::Path &destination, + uint64_t journalSize, + std::string_view commitDevice, + sys::ProgressTask *task) +{ + int zipError = unzGoToFirstFile(source); + if (zipError != UNZ_OK) + { + logger::log("Error opening empty ZIP file: %i.", zipError); + return; + } + + do + { + // Get file information. + unz_file_info64 currentFileInfo; + char filename[FS_MAX_PATH] = {0}; + if (unzGetCurrentFileInfo64(source, ¤tFileInfo, filename, FS_MAX_PATH, NULL, 0, NULL, 0) != UNZ_OK || + unzOpenCurrentFile(source) != UNZ_OK) + { + logger::log("Error opening and getting information for file in zip."); + continue; + } + + // Create full path to item, make sure directories are created if needed. + fslib::Path fullDestination = destination / filename; + + fslib::Path directories = fullDestination.subPath(fullDestination.findLastOf('/') - 1); + // To do: Make FsLib handle this correctly. First condition is a workaround for now... + if (directories.isValid() && !fslib::createDirectoriesRecursively(directories)) + { + logger::log("Error creating zip file path \"%s\": %s", directories.cString(), fslib::getErrorString()); + continue; + } + + fslib::File destinationFile(fullDestination, FsOpenMode_Create | FsOpenMode_Write, currentFileInfo.uncompressed_size); + if (!destinationFile.isOpen()) + { + logger::log("Error creating file from zip: %s", fslib::getErrorString()); + continue; + } + + // Shared data for both threads + std::shared_ptr sharedData(new ZipIOStruct); + sharedData->m_sharedBuffer = std::make_unique(UNZIP_BUFFER_SIZE); + + // Spawn read thread. + std::thread readThread(unzipReadThreadFunction, source, currentFileInfo.uncompressed_size, sharedData); + + // Local buffer + std::unique_ptr localBuffer(new unsigned char[UNZIP_BUFFER_SIZE]); + + // Set status + if (task) + { + task->setStatus(strings::getByName(strings::names::COPYING_FILES, 3), filename); + task->reset(static_cast(currentFileInfo.uncompressed_size)); + } + + for (int64_t writeCount = 0, readCount = 0, journalCount = 0; writeCount < static_cast(currentFileInfo.uncompressed_size);) + { + { + // Wait for buffer. + std::unique_lock m_bufferLock(sharedData->m_bufferLock); + sharedData->m_bufferCondition.wait(m_bufferLock, [&sharedData]() { return sharedData->m_bufferIsFull; }); + + // Save read count for later + readCount = sharedData->m_readCount; + + // Copy shared to local + std::memcpy(localBuffer.get(), sharedData->m_sharedBuffer.get(), readCount); + + // Signal this thread is done. + sharedData->m_bufferIsFull = false; + sharedData->m_bufferCondition.notify_one(); + } + + // Journaling check + if (journalCount + readCount >= static_cast(journalSize)) + { + // Close. + destinationFile.close(); + // Commit + if (!fslib::commitDataToFileSystem(commitDevice)) + { + logger::log("Error committing data to save: %s", fslib::getErrorString()); + } + // Reopen, seek to previous position. + destinationFile.open(fullDestination, FsOpenMode_Write); + destinationFile.seek(writeCount, destinationFile.beginning); + // Reset journal + journalCount = 0; + } + // Write data. + destinationFile.write(localBuffer.get(), readCount); + // Update write and journal count + writeCount += readCount; + journalCount += journalCount; + // Update status + if (task) + { + task->updateCurrent(writeCount); + } + } + // Close file and commit again just for good measure. + destinationFile.close(); + if (!fslib::commitDataToFileSystem(commitDevice)) + { + logger::log("Error performing final file commit: %s", fslib::getErrorString()); + } + } while (unzGoToNextFile(source) != UNZ_END_OF_LIST_OF_FILE); +} diff --git a/source/input.cpp b/source/input.cpp new file mode 100644 index 0000000..26cbc3a --- /dev/null +++ b/source/input.cpp @@ -0,0 +1,32 @@ +#include "input.hpp" + +namespace +{ + PadState s_gamepad; +} + +void input::initialize(void) +{ + padConfigureInput(1, HidNpadStyleSet_NpadStandard); + padInitializeDefault(&s_gamepad); +} + +void input::update(void) +{ + padUpdate(&s_gamepad); +} + +bool input::buttonPressed(HidNpadButton button) +{ + return (s_gamepad.buttons_cur & button) && !(s_gamepad.buttons_old & button); +} + +bool input::buttonHeld(HidNpadButton button) +{ + return (s_gamepad.buttons_cur & button) && (s_gamepad.buttons_old & button); +} + +bool input::buttonReleased(HidNpadButton button) +{ + return (s_gamepad.buttons_old & button) && !(s_gamepad.buttons_cur & button); +} diff --git a/source/keyboard.cpp b/source/keyboard.cpp new file mode 100644 index 0000000..e81547f --- /dev/null +++ b/source/keyboard.cpp @@ -0,0 +1,31 @@ +#include "keyboard.hpp" +#include + +bool keyboard::getInput(SwkbdType keyboardType, std::string_view defaultText, std::string_view header, char *stringOut, size_t stringLength) +{ + // Setup keyboard. + SwkbdConfig keyboard; + swkbdCreate(&keyboard, 0); // Old JKSV actually used dictionary words, but I don't feel like implementing them again. + swkbdConfigSetBlurBackground(&keyboard, true); + swkbdConfigSetInitialText(&keyboard, defaultText.data()); + swkbdConfigSetHeaderText(&keyboard, header.data()); + swkbdConfigSetGuideText(&keyboard, header.data()); + swkbdConfigSetType(&keyboard, keyboardType); + swkbdConfigSetStringLenMax(&keyboard, stringLength); + swkbdConfigSetKeySetDisableBitmask(&keyboard, SwkbdKeyDisableBitmask_ForwardSlash | SwkbdKeyDisableBitmask_Backslash); + + // If it fails, just return. + if (R_FAILED(swkbdShow(&keyboard, stringOut, stringLength))) + { + return false; + } + + // If the string is empty, assume failure or cancel. + if (std::char_traits::length(stringOut) == 0) + { + return false; + } + + // I wish this was more like the 3DS keyboard cause that actually returned what button was pressed... + return true; +} diff --git a/source/logger.cpp b/source/logger.cpp new file mode 100644 index 0000000..aa02ae1 --- /dev/null +++ b/source/logger.cpp @@ -0,0 +1,33 @@ +#include "logger.hpp" +#include "config.hpp" +#include "fslib.hpp" +#include + +namespace +{ + // Path to log file. + fslib::Path s_logFilePath; + // Size of va buffer for log. + constexpr size_t VA_BUFFER_SIZE = 0x1000; +} // namespace + +void logger::initialize(void) +{ + // Create log path and empty the log for this run. + s_logFilePath = "sdmc:/switch/JKSV.log"; + fslib::File LogFile(s_logFilePath, FsOpenMode_Create | FsOpenMode_Write); +} + +void logger::log(const char *format, ...) +{ + char vaBuffer[VA_BUFFER_SIZE] = {0}; + + std::va_list vaList; + va_start(vaList, format); + vsnprintf(vaBuffer, VA_BUFFER_SIZE, format, vaList); + va_end(vaList); + + fslib::File logFile(s_logFilePath, FsOpenMode_Append); + logFile << vaBuffer << "\n"; + logFile.flush(); +} diff --git a/source/main.cpp b/source/main.cpp new file mode 100644 index 0000000..4dec8af --- /dev/null +++ b/source/main.cpp @@ -0,0 +1,15 @@ +#include "JKSV.hpp" +#include "config.hpp" +#include + +int main(void) +{ + JKSV jksv{}; + while (appletMainLoop() && jksv.isRunning()) + { + jksv.update(); + jksv.render(); + } + config::save(); + return 0; +} diff --git a/source/stringUtil.cpp b/source/stringUtil.cpp new file mode 100644 index 0000000..34bf020 --- /dev/null +++ b/source/stringUtil.cpp @@ -0,0 +1,108 @@ +#include "stringUtil.hpp" +#include +#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, ...) +{ + char vaBuffer[VA_BUFFER_SIZE] = {0}; + + std::va_list vaList; + va_start(vaList, format); + vsnprintf(vaBuffer, VA_BUFFER_SIZE, format, vaList); + va_end(vaList); + + 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; +} + +std::string stringutil::getDateString(stringutil::DateFormat format) +{ + char stringBuffer[0x80] = {0}; + + std::time_t timer; + std::time(&timer); + std::tm *localTime = std::localtime(&timer); + + switch (format) + { + case stringutil::DateFormat::YearMonthDay: + { + std::strftime(stringBuffer, 0x80, "%Y-%m-%d_%H-%M-%S", localTime); + } + break; + + case stringutil::DateFormat::YearDayMonth: + { + std::strftime(stringBuffer, 0x80, "%Y-%d-%m_%H-%M-%S", localTime); + } + break; + } + + return std::string(stringBuffer); +} diff --git a/source/strings.cpp b/source/strings.cpp new file mode 100644 index 0000000..356cfd0 --- /dev/null +++ b/source/strings.cpp @@ -0,0 +1,120 @@ +#include "strings.hpp" +#include "JSON.hpp" +#include "fslib.hpp" +#include "stringUtil.hpp" +#include +#include +#include + +namespace +{ + // This is the actual map where the strings are. + std::map, std::string> s_stringMap; + // This map is for matching files to the language value + std::unordered_map s_fileMap = {{SetLanguage_JA, "JA.json"}, + {SetLanguage_ENUS, "ENUS.json"}, + {SetLanguage_FR, "FR.json"}, + {SetLanguage_DE, "DE.json"}, + {SetLanguage_IT, "IT.json"}, + {SetLanguage_ES, "ES.json"}, + {SetLanguage_ZHCN, "ZHCN.json"}, + {SetLanguage_KO, "KO.json"}, + {SetLanguage_NL, "NL.json"}, + {SetLanguage_PT, "PT.json"}, + {SetLanguage_RU, "RU.json"}, + {SetLanguage_ZHTW, "ZHTW.json"}, + {SetLanguage_ENGB, "ENGB.json"}, + {SetLanguage_FRCA, "FRCA.json"}, + {SetLanguage_ES419, "ES419.json"}, + {SetLanguage_ZHHANS, "ZHCN.json"}, + {SetLanguage_ZHHANT, "ZHTW.json"}, + {SetLanguage_PTBR, "PTBR.json"}}; +} // namespace + +// This returns the language file to use depending on the system's language. +static fslib::Path getFilePath(void) +{ + fslib::Path returnPath = "romfs:/Text"; + + uint64_t languageCode = 0; + Result setError = setGetLanguageCode(&languageCode); + if (R_FAILED(setError)) + { + return returnPath / s_fileMap.at(SetLanguage_ENUS); + } + + SetLanguage language; + setError = setMakeLanguage(languageCode, &language); + if (R_FAILED(setError)) + { + return returnPath / s_fileMap.at(SetLanguage_ENUS); + } + 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 filePath = getFilePath(); + + json::Object stringJSON = json::newObject(json_object_from_file, filePath.cString()); + if (!stringJSON) + { + return false; + } + + json_object_iterator stringIterator = json_object_iter_begin(stringJSON.get()); + json_object_iterator stringEnd = json_object_iter_end(stringJSON.get()); + while (!json_object_iter_equal(&stringIterator, &stringEnd)) + { + // Get name of string(s) and pointer to array + const char *stringName = json_object_iter_peek_name(&stringIterator); + json_object *stringArray = json_object_iter_peek_value(&stringIterator); + + // Loop through array and add them to map so I can be lazier and not have to edit code or do shit to add more strings. + size_t arrayLength = json_object_array_length(stringArray); + for (size_t i = 0; i < arrayLength; i++) + { + json_object *string = json_object_array_get_idx(stringArray, i); + s_stringMap[std::make_pair(stringName, static_cast(i))] = json_object_get_string(string); + } + json_object_iter_next(&stringIterator); + } + + // Loop through entire map and replace the buttons. + for (auto &[key, string] : s_stringMap) + { + replaceButtonsInString(string); + } + + return true; +} + +const char *strings::getByName(std::string_view name, int index) +{ + if (s_stringMap.find(std::make_pair(name.data(), index)) == s_stringMap.end()) + { + return nullptr; + } + return s_stringMap.at(std::make_pair(name.data(), index)).c_str(); +} diff --git a/source/system/ProgressTask.cpp b/source/system/ProgressTask.cpp new file mode 100644 index 0000000..aa2526c --- /dev/null +++ b/source/system/ProgressTask.cpp @@ -0,0 +1,22 @@ +#include "system/ProgressTask.hpp" + +void sys::ProgressTask::reset(double goal) +{ + m_current = 0; + m_goal = goal; +} + +void sys::ProgressTask::updateCurrent(double current) +{ + m_current = current; +} + +double sys::ProgressTask::getGoal(void) const +{ + return m_goal; +} + +double sys::ProgressTask::getCurrent(void) const +{ + return m_current / m_goal; +} diff --git a/source/system/Task.cpp b/source/system/Task.cpp new file mode 100644 index 0000000..5ce91ee --- /dev/null +++ b/source/system/Task.cpp @@ -0,0 +1,42 @@ +#include "system/Task.hpp" +#include + +namespace +{ + /// @brief Size of buffer for formatting the status string. + constexpr size_t VA_BUFFER_SIZE = 0x1000; +} // namespace + +sys::Task::~Task() +{ + m_thread.join(); +} + +bool sys::Task::isRunning(void) const +{ + return m_isRunning; +} + +void sys::Task::finished(void) +{ + m_isRunning = false; +} + +void sys::Task::setStatus(const char *format, ...) +{ + char vaBuffer[VA_BUFFER_SIZE] = {0}; + + std::va_list vaList; + va_start(vaList, format); + vsnprintf(vaBuffer, VA_BUFFER_SIZE, format, vaList); + va_end(vaList); + + std::scoped_lock statusLock(m_statusLock); + m_status = vaBuffer; +} + +std::string sys::Task::getStatus(void) +{ + std::scoped_lock StatusLock(m_statusLock); + return m_status; +} diff --git a/source/system/Timer.cpp b/source/system/Timer.cpp new file mode 100644 index 0000000..949f91d --- /dev/null +++ b/source/system/Timer.cpp @@ -0,0 +1,39 @@ +#include "system/Timer.hpp" + +sys::Timer::Timer(uint64_t triggerTicks) +{ + Timer::start(triggerTicks); +} + +sys::Timer &sys::Timer::operator=(const sys::Timer &timer) +{ + m_startingTicks = timer.m_startingTicks; + m_triggerTicks = timer.m_triggerTicks; + return *this; +} + +void sys::Timer::start(uint64_t triggerTicks) +{ + m_startingTicks = SDL_GetTicks64(); + m_triggerTicks = triggerTicks; +} + +bool sys::Timer::isTriggered(void) +{ + uint64_t currentTicks = SDL_GetTicks64(); + + // Nope + if (currentTicks - m_startingTicks < m_triggerTicks) + { + return false; + } + // Reset starting ticks. + m_startingTicks = currentTicks; + // Trigger me timbers~ + return true; +} + +void sys::Timer::restart(void) +{ + m_startingTicks = SDL_GetTicks64(); +} diff --git a/source/ui/ColorMod.cpp b/source/ui/ColorMod.cpp new file mode 100644 index 0000000..8e43dd8 --- /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/IconMenu.cpp b/source/ui/IconMenu.cpp new file mode 100644 index 0000000..b3b6a76 --- /dev/null +++ b/source/ui/IconMenu.cpp @@ -0,0 +1,42 @@ +#include "ui/IconMenu.hpp" +#include "colors.hpp" +#include "ui/renderFunctions.hpp" + +ui::IconMenu::IconMenu(int x, int y, int rendertargetHeight) : Menu(x, y, 152, 80, rendertargetHeight) {}; + +void ui::IconMenu::update(bool hasFocus) +{ + Menu::update(hasFocus); +} + +void ui::IconMenu::render(SDL_Texture *target, bool hasFocus) +{ + if (hasFocus) + { + m_colorMod.update(); + } + + for (int i = 0, tempY = m_y; i < static_cast(m_options.size()); i++, tempY += m_optionHeight) + { + // Clear target. + m_optionTarget->clear(colors::TRANSPARENT); + if (i == m_selected) + { + if (hasFocus) + { + ui::renderBoundingBox(target, m_x - 8, tempY - 8, 152, 146, m_colorMod); + } + sdl::renderRectFill(m_optionTarget->get(), 0, 0, 4, 130, {0x00FFC5FF}); + } + //m_options.at(i)->render(m_optiontarget->Get(), 0, 0); + m_options.at(i)->renderStretched(m_optionTarget->get(), 8, 1, 128, 128); + m_optionTarget->render(target, m_x, tempY); + } +} + +void ui::IconMenu::addOption(sdl::SharedTexture newOption) +{ + // Parent needs a text option to work correctly. + Menu::addOption("ICON"); + m_options.push_back(newOption); +} diff --git a/source/ui/Menu.cpp b/source/ui/Menu.cpp new file mode 100644 index 0000000..f8fbf91 --- /dev/null +++ b/source/ui/Menu.cpp @@ -0,0 +1,157 @@ +#include "ui/Menu.hpp" +#include "colors.hpp" +#include "config.hpp" +#include "input.hpp" +#include "ui/renderFunctions.hpp" +#include + +ui::Menu::Menu(int x, int y, int width, int fontSize, int renderTargetHeight) + : m_x(x), m_y(y), m_optionHeight(std::ceil(static_cast(fontSize) * 1.8f)), m_originalY(y), m_targetY(y), m_width(width), + m_fontSize(fontSize), m_renderTargetHeight(renderTargetHeight) +{ + // Create render target for options + static int MENU_ID = 0; + std::string menuTargetName = "MENU_" + std::to_string(MENU_ID++); + m_optionTarget = + sdl::TextureManager::createLoadTexture(menuTargetName, m_width, m_optionHeight, SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET); + + // Calculate around how many options can be shown on the render target at once. + m_maxDisplayOptions = (renderTargetHeight - m_originalY) / m_optionHeight; + m_scrollLength = std::floor(static_cast(m_maxDisplayOptions) / 2.0f); +} + +void ui::Menu::update(bool hasFocus) +{ + // Bail if there's nothing to update. + if (m_options.empty()) + { + return; + } + + int optionsSize = m_options.size(); + if (input::buttonPressed(HidNpadButton_AnyUp) && --m_selected < 0) + { + m_selected = optionsSize - 1; + } + else if (input::buttonPressed(HidNpadButton_AnyDown) && ++m_selected >= optionsSize) + { + m_selected = 0; + } + else if (input::buttonPressed(HidNpadButton_AnyLeft) && (m_selected -= m_scrollLength) < 0) + { + m_selected = 0; + } + else if (input::buttonPressed(HidNpadButton_AnyRight) && (m_selected += m_scrollLength) >= optionsSize) + { + m_selected = optionsSize - 1; + } + else if (input::buttonPressed(HidNpadButton_L) && (m_selected -= m_scrollLength * 3) < 0) + { + m_selected = 0; + } + else if (input::buttonPressed(HidNpadButton_R) && (m_selected += m_scrollLength * 3) >= optionsSize) + { + m_selected = optionsSize - 1; + } + + // Don't bother continuing further if there's no reason to scroll. + if (static_cast(m_options.size()) <= m_maxDisplayOptions) + { + return; + } + + if (m_selected < m_scrollLength) + { + m_targetY = m_originalY; + } + else if (m_selected >= static_cast(m_options.size()) - (m_maxDisplayOptions - m_scrollLength)) + { + m_targetY = m_originalY - ((m_options.size() - m_maxDisplayOptions) * m_optionHeight); + } + else if (m_selected >= m_scrollLength) + { + m_targetY = m_originalY - ((m_selected - m_scrollLength) * m_optionHeight); + } + + if (m_y != m_targetY) + { + m_y += std::ceil((m_targetY - m_y) / config::getAnimationScaling()); + } +} + +void ui::Menu::render(SDL_Texture *target, bool hasFocus) +{ + 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 < static_cast(m_options.size()); i++, tempY += m_optionHeight) + { + if (tempY < -m_fontSize) + { + continue; + } + else if (tempY > m_renderTargetHeight) + { + // This is safe to break the loop for. + break; + } + + // Clear target texture. + m_optionTarget->clear(colors::TRANSPARENT); + + if (i == m_selected) + { + if (hasFocus) + { + // render the bounding box + ui::renderBoundingBox(target, m_x - 4, tempY - 4, m_width + 8, m_optionHeight + 8, m_colorMod); + } + // render the little rectangle. + sdl::renderRectFill(m_optionTarget->get(), 8, 8, 4, m_optionHeight - 16, colors::BLUE_GREEN); + } + // render text to target. + sdl::text::render(m_optionTarget->get(), + 24, + (m_optionHeight / 2) - (m_fontSize / 2), + m_fontSize, + sdl::text::NO_TEXT_WRAP, + i == m_selected && hasFocus ? colors::BLUE_GREEN : 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()); +} + +int ui::Menu::getSelected(void) const +{ + return m_selected; +} + +void ui::Menu::setSelected(int selected) +{ + m_selected = selected; +} + +void ui::Menu::setWidth(int width) +{ + m_width = width; +} + +void ui::Menu::reset(void) +{ + m_selected = 0; + m_y = m_originalY; + m_options.clear(); +} diff --git a/source/ui/PopMessageManager.cpp b/source/ui/PopMessageManager.cpp new file mode 100644 index 0000000..e1b8c9d --- /dev/null +++ b/source/ui/PopMessageManager.cpp @@ -0,0 +1,104 @@ +#include "ui/PopMessageManager.hpp" +#include "colors.hpp" +#include "config.hpp" +#include "logger.hpp" +#include "sdl.hpp" +#include "ui/renderFunctions.hpp" +#include + +namespace +{ + // Size of the buffer for va strings. + constexpr int VA_BUFFER_SIZE = 0x200; + +} // namespace + +void ui::PopMessageManager::update(void) +{ + // Grab instance. + PopMessageManager &manager = PopMessageManager::getInstance(); + + // Bail if the queue is empty. + if (!manager.m_messageQueue.empty()) + { + // Loop through the queue and process it so we don't wind up with black characters. + for (auto &[displayTicks, currentMessage] : manager.m_messageQueue) + { + // New message. + manager.m_messages.push_back({.m_y = 720, + .m_targetY = 720, + .m_width = sdl::text::getWidth(32, currentMessage.c_str()) + 32, + .m_message = currentMessage, + .m_timer = sys::Timer(displayTicks)}); + } + // Clear the queue. + manager.m_messageQueue.clear(); + } + + // Update all the messages. + // This is the first Y position a message should be displayed at.; + double currentY = 594.0f; + double animationScaling = config::getAnimationScaling(); + for (size_t i = 0; i < manager.m_messages.size(); i++) + { + // Save myself a shit load of typing. + ui::PopMessage ¤tMessage = manager.m_messages.at(i); + + // Purge it and continue if needed. + if (currentMessage.m_timer.isTriggered()) + { + manager.m_messages.erase(manager.m_messages.begin() + i); + continue; + } + + // Make sure Y coordinate is correct. + if (currentMessage.m_targetY != currentY) + { + currentMessage.m_targetY = currentY; + } + + if (currentMessage.m_y != currentMessage.m_targetY) + { + currentMessage.m_y += (currentMessage.m_targetY - currentMessage.m_y) / animationScaling; + } + currentY -= 52; + } +} + +void ui::PopMessageManager::render(void) +{ + // Get instance. + PopMessageManager &manager = PopMessageManager::getInstance(); + + // Loop and render. + for (auto &popMessage : manager.m_messages) + { + // Render a dialog box around it. + ui::renderDialogBox(NULL, 20, popMessage.m_y - 4, popMessage.m_width, 48); + // Render the actual text. + sdl::text::render(NULL, 36, popMessage.m_y, 32, sdl::text::NO_TEXT_WRAP, colors::WHITE, popMessage.m_message.c_str()); + } +} + +void ui::PopMessageManager::pushMessage(int displayTicks, const char *format, ...) +{ + // VA args. + char vaBuffer[VA_BUFFER_SIZE] = {0}; + + std::va_list vaList; + va_start(vaList, format); + vsnprintf(vaBuffer, VA_BUFFER_SIZE, format, vaList); + va_end(vaList); + + // Get instance. + PopMessageManager &manager = PopMessageManager::getInstance(); + + // Make sure we're not pushing two of the same message. + if (!manager.m_messages.empty() && manager.m_messages.back().m_message.compare(vaBuffer) == 0) + { + // Bail and don't push it to the queue because it matches. + return; + } + // Push it to the queue. + manager.m_messageQueue.push_back(std::make_pair(displayTicks, vaBuffer)); +} diff --git a/source/ui/SlideOutPanel.cpp b/source/ui/SlideOutPanel.cpp new file mode 100644 index 0000000..9f70fe6 --- /dev/null +++ b/source/ui/SlideOutPanel.cpp @@ -0,0 +1,91 @@ +#include "ui/SlideOutPanel.hpp" +#include "colors.hpp" +#include "config.hpp" +#include + +ui::SlideOutPanel::SlideOutPanel(int width, Side side) : m_x(side == Side::Left ? -width : 1280), m_width(width), m_side(side) +{ + static int slidePanelTargetID = 0; + std::string panelTargetName = "PanelTarget_" + std::to_string(slidePanelTargetID++); + m_renderTarget = sdl::TextureManager::createLoadTexture(panelTargetName, width, 720, SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET); +} + +void ui::SlideOutPanel::update(bool hasFocus) +{ + double scaling = config::getAnimationScaling(); + + if (!m_isOpen && m_side == Side::Left && m_x < 0) + { + m_x -= std::ceil(m_x / scaling); + } + else if (!m_isOpen && m_side == Side::Right && m_x > 1280 - m_width) + { + m_x += std::ceil((1280.0f - (static_cast(m_width)) - m_x) / scaling); + } + else if (m_closePanel && m_side == Side::Left && m_x > -(m_width)) + { + m_x -= std::ceil((m_width - m_x) / scaling); + } + else if (m_closePanel && m_side == Side::Right && m_x < 1280) + { + m_x += std::ceil((1280.0f - m_x) / scaling); + } + else + { + m_isOpen = true; + } + + if (hasFocus && m_isOpen) + { + for (auto ¤tElement : m_elements) + { + currentElement->update(hasFocus); + } + } +} + +void ui::SlideOutPanel::render(SDL_Texture *Target, bool hasFocus) +{ + for (auto ¤tElement : m_elements) + { + currentElement->render(m_renderTarget->get(), hasFocus); + } + m_renderTarget->render(NULL, m_x, 0); +} + +void ui::SlideOutPanel::clearTarget(void) +{ + m_renderTarget->clear(colors::SLIDE_PANEL_CLEAR); +} + +void ui::SlideOutPanel::reset(void) +{ + m_x = m_side == Side::Left ? -(m_width) : 1280.0f; + m_isOpen = false; + m_closePanel = false; +} + +void ui::SlideOutPanel::close(void) +{ + m_closePanel = true; +} + +bool ui::SlideOutPanel::isOpen(void) const +{ + return m_isOpen; +} + +bool ui::SlideOutPanel::isClosed(void) const +{ + return m_closePanel && (m_side == Side::Left ? m_x > -(m_width) : m_x < 1280); +} + +void ui::SlideOutPanel::pushNewElement(std::shared_ptr newElement) +{ + m_elements.push_back(newElement); +} + +SDL_Texture *ui::SlideOutPanel::get(void) +{ + return m_renderTarget->get(); +} diff --git a/source/ui/TitleTile.cpp b/source/ui/TitleTile.cpp new file mode 100644 index 0000000..65e8061 --- /dev/null +++ b/source/ui/TitleTile.cpp @@ -0,0 +1,47 @@ +#include "ui/TitleTile.hpp" +#include "colors.hpp" + +ui::TitleTile::TitleTile(bool isFavorite, sdl::SharedTexture icon) : m_isFavorite(isFavorite), m_icon(icon) {}; + +void ui::TitleTile::update(bool isSelected) +{ + if (isSelected && m_renderWidth < 164) + { + // I think it's safe to assume both are too small. + m_renderWidth += 18; + m_renderHeight += 18; + } + else if (!isSelected && m_renderWidth > 128) + { + m_renderWidth -= 9; + m_renderHeight -= 9; + } +} + +void ui::TitleTile::render(SDL_Texture *target, int x, int y) +{ + int renderX = x - ((m_renderWidth - 128) / 2); + int renderY = y - ((m_renderHeight - 128) / 2); + + m_icon->renderStretched(target, renderX, renderY, m_renderWidth, m_renderHeight); + if (m_isFavorite) + { + sdl::text::render(target, renderX + 4, renderY + 2, 28, sdl::text::NO_TEXT_WRAP, colors::PINK, "\uE017"); + } +} + +void ui::TitleTile::reset(void) +{ + m_renderWidth = 128; + m_renderHeight = 128; +} + +int ui::TitleTile::getWidth(void) const +{ + return m_renderWidth; +} + +int ui::TitleTile::getHeight(void) const +{ + return m_renderHeight; +} diff --git a/source/ui/TitleView.cpp b/source/ui/TitleView.cpp new file mode 100644 index 0000000..8a01393 --- /dev/null +++ b/source/ui/TitleView.cpp @@ -0,0 +1,135 @@ +#include "ui/TitleView.hpp" +#include "colors.hpp" +#include "config.hpp" +#include "input.hpp" +#include "logger.hpp" +#include "ui/renderFunctions.hpp" +#include + +namespace +{ + constexpr int ICON_ROW_SIZE = 7; +} + +ui::TitleView::TitleView(data::User *user) : m_user(user) +{ + TitleView::refresh(); +} + +void ui::TitleView::update(bool hasFocus) +{ + if (m_titleTiles.empty()) + { + return; + } + + // Update pulse + if (hasFocus) + { + m_colorMod.update(); + } + + // Input. + int totalTiles = m_titleTiles.size() - 1; + if (input::buttonPressed(HidNpadButton_AnyUp) && (m_selected -= ICON_ROW_SIZE) < 0) + { + m_selected = 0; + } + else if (input::buttonPressed(HidNpadButton_AnyDown) && (m_selected += ICON_ROW_SIZE) > totalTiles) + { + m_selected = totalTiles; + } + else if (input::buttonPressed(HidNpadButton_AnyLeft) && m_selected > 0) + { + --m_selected; + } + else if (input::buttonPressed(HidNpadButton_AnyRight) && m_selected < totalTiles) + { + ++m_selected; + } + else if (input::buttonPressed(HidNpadButton_L) && (m_selected -= 21) < 0) + { + m_selected = 0; + } + else if (input::buttonPressed(HidNpadButton_R) && (m_selected += 21) > totalTiles) + { + m_selected = totalTiles; + } + + double scaling = config::getAnimationScaling(); + if (m_selectedY > 388.0f) + { + m_y += std::ceil((388.0f - m_selectedY) / scaling); + } + else if (m_selectedY < 28.0f) + { + m_y += std::ceil((28.0f - m_selectedY) / scaling); + } + + for (size_t i = 0; i < m_titleTiles.size(); i++) + { + m_titleTiles.at(i).update(m_selected == static_cast(i) ? true : false); + } +} + +void ui::TitleView::render(SDL_Texture *target, bool hasFocus) +{ + if (m_titleTiles.empty()) + { + return; + } + + for (int i = 0, tempY = m_y; i < static_cast(m_titleTiles.size()); tempY += 144) + { + int endRow = i + 7; + for (int j = i, tempX = 32; j < endRow; j++, i++, tempX += 144) + { + if (i >= static_cast(m_titleTiles.size())) + { + break; + } + + // Save the X and Y to render the selected tile over the rest. + if (i == m_selected) + { + m_selectedX = tempX; + m_selectedY = tempY; + continue; + } + // Just render + m_titleTiles.at(i).render(target, tempX, tempY); + } + } + // Now render the selected title. + if (hasFocus) + { + sdl::renderRectFill(target, m_selectedX - 23, m_selectedY - 23, 174, 174, colors::CLEAR_COLOR); + ui::renderBoundingBox(target, m_selectedX - 24, m_selectedY - 24, 176, 176, m_colorMod); + } + m_titleTiles.at(m_selected).render(target, m_selectedX, m_selectedY); +} + +int ui::TitleView::getSelected(void) const +{ + return m_selected; +} + +void ui::TitleView::refresh(void) +{ + m_titleTiles.clear(); + for (size_t i = 0; i < m_user->getTotalDataEntries(); i++) + { + // Get pointer to data from user save index I. + data::TitleInfo *currentTitleInfo = data::getTitleInfoByID(m_user->getApplicationIDAt(i)); + // Emplace is faster than push + m_titleTiles.emplace_back(config::isFavorite(m_user->getApplicationIDAt(i)), currentTitleInfo->getIcon()); + } +} + +void ui::TitleView::reset(void) +{ + for (ui::TitleTile ¤tTile : m_titleTiles) + { + currentTile.reset(); + } +} diff --git a/source/ui/renderFunctions.cpp b/source/ui/renderFunctions.cpp new file mode 100644 index 0000000..5a3c5cb --- /dev/null +++ b/source/ui/renderFunctions.cpp @@ -0,0 +1,53 @@ +#include "ui/renderFunctions.hpp" +#include "colors.hpp" + +namespace +{ + sdl::SharedTexture s_dialogCorners = nullptr; + sdl::SharedTexture s_menuBoundingCorners = nullptr; +} // namespace + +void ui::renderDialogBox(SDL_Texture *target, int x, int y, int width, int height) +{ + if (!s_dialogCorners) + { + s_dialogCorners = sdl::TextureManager::createLoadTexture("DialogCorners", "romfs:/Textures/DialogCorners.png"); + } + + // Top + s_dialogCorners->renderPart(target, x, y, 0, 0, 16, 16); + sdl::renderRectFill(target, x + 16, y, width - 32, 16, colors::DIALOG_BOX); + s_dialogCorners->renderPart(target, (x + width) - 16, y, 16, 0, 16, 16); + // Middle + sdl::renderRectFill(NULL, x, y + 16, width, height - 32, colors::DIALOG_BOX); + // Bottom + s_dialogCorners->renderPart(target, x, (y + height) - 16, 0, 16, 16, 16); + sdl::renderRectFill(NULL, x + 16, (y + height) - 16, width - 32, 16, colors::DIALOG_BOX); + 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 = {static_cast((0x88 + colorMod) << 16 | (0xC5 + (colorMod / 2)) << 8 | 0xFF)}; + + // This shouldn't fail, but I don't really care if it does. + s_menuBoundingCorners->setColorMod(renderMod); + + // 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); +}