Data partially implemented with no crashes. Getting too tired.

This commit is contained in:
J-D-K
2024-12-06 20:44:47 -05:00
parent 7816dab291
commit cf3cb7a219
29 changed files with 1150 additions and 13 deletions

View File

@@ -6,11 +6,17 @@ class AppState
{
public:
AppState(void) = default;
virtual ~AppState() = 0;
virtual ~AppState() {};
virtual void Update(void) = 0;
virtual void Render(void) = 0;
// Allows the state to be reactivated if needed.
void Reactivate(void)
{
m_IsActive = true;
}
// Returns whether or not state is still active or can be purged.
bool IsActive(void) const
{

View File

@@ -0,0 +1,23 @@
#pragma once
#include "AppStates/AppState.hpp"
#include "SDL.hpp"
class MainMenuState : public AppState
{
public:
MainMenuState(void);
~MainMenuState() {};
void Update(void);
void Render(void);
private:
// The render target.
SDL::SharedTexture m_RenderTarget = nullptr;
// Background of the menu.
SDL::SharedTexture m_Background = nullptr;
// Pointer to control guide string so I don't need to call and fetch it every loop.
const char *m_ControlGuide = nullptr;
// X coordinate to render controls at.
int m_ControlGuideX;
};

View File

@@ -13,4 +13,5 @@ namespace Colors
static constexpr SDL::Color ClearColor = {0x2D2D2DFF};
static constexpr SDL::Color DialogBox = {0x505050FF};
static constexpr SDL::Color BackgroundDim = {0x00000088};
static constexpr SDL::Color Transparent = {0x00000000};
} // namespace Colors

52
Include/Config.hpp Normal file
View File

@@ -0,0 +1,52 @@
#pragma once
#include "FsLib.hpp"
#include <string_view>
namespace Config
{
// Attempts to load config from file. If that fails, initializes to default.
void Initialize(void);
// Resets config to default settings.
void ResetToDefault(void);
// Saves config.
void Save(void);
// Retrieves a value with its key.
uint8_t GetByKey(std::string_view Key);
// Gets the working directory.
FsLib::Path GetWorkingDirectory(void);
// Gets the UI's transition scaling
double GetAnimationScaling(void);
// Adds or removes title from favorites.
void AddRemoveFavorite(uint64_t TitleID);
// Returns if TitleID is in favorites list.
bool IsFavorite(uint64_t TitleID);
// Adds or removes a title from the blacklist
void AddRemoveBlacklist(uint64_t TitleID);
// Returns if TitleID is in blacklist
bool IsBlacklisted(uint64_t TitleID);
// Names of keys. Note: Not all of these are retrievable with GetByKey. Some of these are purely for config reading and writing.
namespace Keys
{
static constexpr std::string_view WorkingDirectory = "WorkingDirectory";
static constexpr std::string_view IncludeDeviceSaves = "IncludeDeviceSaves";
static constexpr std::string_view AutoBackupOnRestore = "AutoBackupOnRestore";
static constexpr std::string_view HoldForDeletion = "HoldForDeletion";
static constexpr std::string_view HoldForRestoration = "HoldForRestoration";
static constexpr std::string_view HoldForOverwrite = "HoldForOverWrite";
static constexpr std::string_view OnlyListMountable = "OnlyListMountable";
static constexpr std::string_view ListAccountSystemSaves = "ListAccountSystemSaves";
static constexpr std::string_view AllowSystemSaveWriting = "AllowSystemSaveWriting";
static constexpr std::string_view ExportToZip = "ExportToZip";
static constexpr std::string_view ForceEnglish = "ForceEnglish";
static constexpr std::string_view EnableTrashBin = "EnableTrash";
static constexpr std::string_view AutoNameBackups = "AutoNameBackups";
static constexpr std::string_view TitleSortType = "TitleSortType";
static constexpr std::string_view Favorites = "Favorites";
static constexpr std::string_view BlackList = "BlackList";
static constexpr std::string_view AutoUpload = "AutoUploadToRemote";
static constexpr std::string_view UIAnimationScaling = "UIAnimationScaling";
} // namespace Keys
} // namespace Config

View File

@@ -0,0 +1,12 @@
#pragma once
#include <switch.h>
static inline bool operator==(AccountUid AccountID1, AccountUid AccountID2)
{
return (AccountID1.uid[0] == AccountID2.uid[0]) && (AccountID1.uid[1] == AccountID2.uid[1]);
}
static inline u128 AccountUIDToU128(AccountUid AccountID)
{
return (static_cast<u128>(AccountID.uid[0]) << 64 | AccountID.uid[1]);
}

View File

@@ -1,4 +1,7 @@
#pragma once
#include "Data/AccountUID.hpp"
#include "Data/TitleInfo.hpp"
#include "Data/User.hpp"
namespace Data
{

View File

@@ -0,0 +1,28 @@
#pragma once
#include "SDL.hpp"
#include <cstdint>
#include <switch.h>
namespace Data
{
class TitleInfo
{
public:
// Loads control data and icon.
TitleInfo(uint64_t ApplicationID);
// Returns title.
const char *GetTitle(void);
// Returns publisher
const char *GetPublisher(void);
// Returns icon
SDL::SharedTexture GetIcon(void) const;
private:
// This is where all the important stuff is.
NacpStruct m_NACP;
// This is the icon.
SDL::SharedTexture m_Icon = nullptr;
};
} // namespace Data

41
Include/Data/User.hpp Normal file
View File

@@ -0,0 +1,41 @@
#pragma once
#include "SDL.hpp"
#include <string>
#include <string_view>
#include <switch.h>
#include <unordered_map>
namespace Data
{
class User
{
public:
// This is for normal use accounts.
User(AccountUid AccountID);
// This is for system type accounts.
User(AccountUid AccountID, std::string_view Nickname, std::string_view PathSafeNickname);
// Adds Data to UserDataMap
void AddToMap(const FsSaveDataInfo &SaveInfo, const PdmPlayStatistics &PlayStats);
// Returns FsSaveDataInfo by ApplicationID or SystemSaveID.
FsSaveDataInfo *GetSaveInfoByID(uint64_t ApplicationID);
// Returns PlayStats according to ^
PdmPlayStatistics *GetPlayStatsByID(uint64_t ApplicationID);
private:
// User's ID.
AccountUid m_AccountID;
// Nickname
char m_Nickname[0x20];
// Path safe nickname.
char m_PathSafeNickname[0x20];
// User's icon
SDL::SharedTexture m_Icon = nullptr;
// Map of FsSaveInfo and play stats
std::unordered_map<uint64_t, std::pair<FsSaveDataInfo, PdmPlayStatistics>> m_UserDataMap;
// Loads account using profile structs.
void LoadAccount(AccountProfile &Profile, AccountProfileBase &ProfileBase);
// Creates a placeholder since something went wrong getting profile info.
void CreateAccount(void);
};
} // namespace Data

View File

@@ -3,5 +3,10 @@
namespace StringUtil
{
// Gets a string formatted with va args.
std::string GetFormattedString(const char *Format, ...);
}
// Replaces a sequence of characters in a string with another.
void ReplaceInString(std::string &Target, std::string_view Find, std::string_view Replace);
// Tries to make string path safe. Returns false if it's not possible.
bool SanitizeStringForPath(const char *StringIn, char *StringOut, size_t StringOutSize);
} // namespace StringUtil

View File

@@ -11,5 +11,7 @@ namespace Strings
namespace Names
{
static constexpr std::string_view TranslationInfo = "TranslationInfo";
static constexpr std::string_view ControlGuides = "ControlGuides";
static constexpr std::string_view SaveDataTypes = "SaveDataTypes";
} // namespace Names
} // namespace Strings

23
Include/UI/ColorMod.hpp Normal file
View File

@@ -0,0 +1,23 @@
#pragma once
#include <cstdint>
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

15
Include/UI/Element.hpp Normal file
View File

@@ -0,0 +1,15 @@
#pragma once
#include "SDL.hpp"
namespace UI
{
class Element
{
public:
Element(void) = default;
virtual ~Element() {};
virtual void Update(void) = 0;
virtual void Render(SDL_Texture *Target);
};
} // namespace UI

14
Include/UI/IconMenu.hpp Normal file
View File

@@ -0,0 +1,14 @@
#pragma once
#include "UI/Menu.hpp"
namespace UI
{
class IconMenu : public UI::Menu
{
public:
IconMenu(int X, int Y, int ScrollLength);
private:
std::vector<SDL::SharedTexture> m_Options;
};
} // namespace UI

53
Include/UI/Menu.hpp Normal file
View File

@@ -0,0 +1,53 @@
#pragma once
#include "SDL.hpp"
#include "UI/ColorMod.hpp"
#include "UI/Element.hpp"
#include <string>
#include <vector>
namespace UI
{
class Menu : public UI::Element
{
public:
// Coordinates to render to and dimensions of menu.
Menu(int X, int Y, int Width, int FontSize, int ScrollLength);
~Menu() {};
// Required functions from Element.
void Update(void);
void Render(SDL_Texture *Target);
// Adds an option to the menu.
void AddOption(std::string_view NewOption);
// Returns the index of the selected item.
int GetSelected(void) const;
// Sets the selected option
void SetSelected(int Selected);
// Resets the menu.
void Reset(void);
private:
// X and Y coordinates.
double m_X, m_Y;
// These are used for the scrolling effect.
double m_OriginalY, m_TargetY;
// Font size
int m_FontSize;
// How many options before scrolling starts happening.
int m_ScrollLength, m_MenuRenderLength;
// Width. Height is calculated on construction.
int m_Width, m_Height;
// Selected option.
int m_Selected = 0;
// Actual length of the menu.
int m_OptionsLength = -1;
// Color mod for rendering bounding box.
UI::ColorMod m_ColorMod;
// A small target to render the option to so it can't draw text outside of the bounding area.
SDL::SharedTexture m_OptionTarget = nullptr;
// Vector of options.
std::vector<std::string> m_Options;
};
} // namespace UI

View File

@@ -5,5 +5,5 @@
namespace UI
{
void RenderDialogBox(SDL_Texture *Target, int X, int Y, int Width, int Height);
void RenderBoundingBox(SDL_Texture *Target, int X, int Y, int Width, int Height);
void RenderBoundingBox(SDL_Texture *Target, int X, int Y, int Width, int Height, uint8_t ColorMod);
} // namespace UI

View File

@@ -32,7 +32,7 @@ include $(DEVKITPRO)/libnx/switch_rules
#---------------------------------------------------------------------------------
TARGET := JKSV
BUILD := build
SOURCES := Source Source/AppStates Source/UI
SOURCES := Source Source/AppStates Source/UI Source/Data
DATA := data
INCLUDES := Include ./Libraries/FsLib/Switch/FsLib/include ./Libraries/SDLLib/SDL/include
EXEFS_SRC := exefs_src

View File

@@ -2,5 +2,20 @@
"TranslationInfo" : [
"Translated By: %s",
"NULL"
],
"ControlGuides" : [
"[A] Select [Y] Dump All Saves [X] User Options",
"[A] Select [L][R] Jump [Y] Favorite [X] Title Options [B] Back",
"[A] Select [Y] Restore [X] Delete [ZR] Upload [B] Close",
"[A] Toggle [X] Defaults [B] Back"
],
"SaveDataTypes" : [
"System",
"Account",
"BCAT",
"Device",
"Temporary",
"Cache",
"System BCAT"
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View File

@@ -0,0 +1,28 @@
#include "AppStates/MainMenuState.hpp"
#include "Colors.hpp"
#include "SDL.hpp"
#include "Strings.hpp"
MainMenuState::MainMenuState(void)
: m_RenderTarget(SDL::TextureManager::CreateLoadTexture("MainMenuTarget", 200, 555, SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET)),
m_Background(SDL::TextureManager::CreateLoadTexture("MainMenuBackground", "romfs:/Textures/MenuBackground.png")),
m_ControlGuide(Strings::GetByName(Strings::Names::ControlGuides, 0)), m_ControlGuideX(1220 - SDL::Text::GetWidth(20, m_ControlGuide)) {};
void MainMenuState::Update(void)
{
}
void MainMenuState::Render(void)
{
// Clear render target by rendering background to it.
m_Background->Render(m_RenderTarget->Get(), 0, 0);
// Render target to screen.
m_RenderTarget->Render(NULL, 0, 91);
// Control Guide.
if (AppState::HasFocus())
{
SDL::Text::Render(NULL, m_ControlGuideX, 673, 20, SDL::Text::NO_TEXT_WRAP, Colors::White, m_ControlGuide);
}
}

217
Source/Config.cpp Normal file
View File

@@ -0,0 +1,217 @@
#include "Config.hpp"
#include "JSON.hpp"
#include "Logger.hpp"
#include <algorithm>
#include <cstdint>
#include <cstring>
#include <string>
#include <unordered_map>
#include <vector>
namespace
{
// Config path(s)
const char *CONFIG_FOLDER = "sdmc:/config/JKSV";
const char *CONFIG_PATH = "sdmc:/config/JKSV/JKSV.json";
// Map of config values
std::unordered_map<std::string, uint8_t> s_ConfigMap;
// Working directory
FsLib::Path s_WorkingDirectory;
// UI animation scaling.
double s_UIAnimationScaling;
// Vector of favorite title ids
std::vector<uint64_t> s_Favorites;
// Vector of titles to ignore.
std::vector<uint64_t> s_Blacklist;
} // namespace
static void ReadArrayToVector(std::vector<uint64_t> &Vector, json_object *Array)
{
// Just in case. Shouldn't happen though.
Vector.clear();
size_t ArrayLength = json_object_array_length(Array);
for (size_t i = 0; i < ArrayLength; i++)
{
json_object *ArrayEntry = json_object_array_get_idx(Array, i);
if (!ArrayEntry)
{
continue;
}
Vector.push_back(json_object_get_uint64(ArrayEntry));
}
}
void Config::Initialize(void)
{
if (!FsLib::DirectoryExists(CONFIG_FOLDER) && !FsLib::CreateDirectoriesRecursively(CONFIG_FOLDER))
{
Logger::Log("Error creating config folder: %s.", FsLib::GetErrorString());
Config::ResetToDefault();
return;
}
JSON::Object ConfigJSON = JSON::NewObject(json_object_from_file, CONFIG_PATH);
if (!ConfigJSON)
{
Logger::Log("Error opening config for reading: %s", FsLib::GetErrorString());
Config::ResetToDefault();
return;
}
json_object_iterator ConfigIterator = json_object_iter_begin(ConfigJSON.get());
json_object_iterator ConfigEnd = json_object_iter_end(ConfigJSON.get());
while (!json_object_iter_equal(&ConfigIterator, &ConfigEnd))
{
const char *KeyName = json_object_iter_peek_name(&ConfigIterator);
json_object *ConfigValue = json_object_iter_peek_value(&ConfigIterator);
// These are exemptions.
if (std::strcmp(KeyName, Config::Keys::WorkingDirectory.data()) == 0)
{
s_WorkingDirectory = json_object_get_string(ConfigValue);
}
else if (std::strcmp(KeyName, Config::Keys::UIAnimationScaling.data()) == 0)
{
s_UIAnimationScaling = json_object_get_double(ConfigValue);
}
else if (std::strcmp(KeyName, Config::Keys::Favorites.data()) == 0)
{
ReadArrayToVector(s_Favorites, ConfigValue);
}
else if (std::strcmp(KeyName, Config::Keys::BlackList.data()) == 0)
{
ReadArrayToVector(s_Blacklist, ConfigValue);
}
else
{
s_ConfigMap[KeyName] = json_object_get_uint64(ConfigValue);
}
json_object_iter_next(&ConfigIterator);
}
}
void Config::ResetToDefault(void)
{
s_WorkingDirectory = "sdmc:/JKSV";
s_ConfigMap[Config::Keys::IncludeDeviceSaves.data()] = 1;
s_ConfigMap[Config::Keys::AutoBackupOnRestore.data()] = 1;
s_ConfigMap[Config::Keys::HoldForDeletion.data()] = 1;
s_ConfigMap[Config::Keys::HoldForRestoration.data()] = 1;
s_ConfigMap[Config::Keys::HoldForOverwrite.data()] = 1;
s_ConfigMap[Config::Keys::OnlyListMountable.data()] = 1;
s_ConfigMap[Config::Keys::ListAccountSystemSaves.data()] = 0;
s_ConfigMap[Config::Keys::AllowSystemSaveWriting.data()] = 0;
s_ConfigMap[Config::Keys::ExportToZip.data()] = 1;
s_ConfigMap[Config::Keys::ForceEnglish.data()] = 0;
s_ConfigMap[Config::Keys::EnableTrashBin.data()] = 1;
s_ConfigMap[Config::Keys::AutoNameBackups.data()] = 0;
s_ConfigMap[Config::Keys::TitleSortType.data()] = 0;
s_ConfigMap[Config::Keys::AutoUpload.data()] = 0;
s_UIAnimationScaling = 2.5f;
}
void Config::Save(void)
{
JSON::Object ConfigJSON = JSON::NewObject(json_object_new_object);
// Add working directory first.
json_object *WorkingDirectory = json_object_new_string(s_WorkingDirectory.CString());
json_object_object_add(ConfigJSON.get(), Config::Keys::WorkingDirectory.data(), WorkingDirectory);
// Loop through map and add it.
for (auto &[Key, Value] : s_ConfigMap)
{
json_object *JsonValue = json_object_new_uint64(Value);
json_object_object_add(ConfigJSON.get(), Key.c_str(), JsonValue);
}
// Add UI scaling.
json_object *Scaling = json_object_new_double(s_UIAnimationScaling);
json_object_object_add(ConfigJSON.get(), Config::Keys::UIAnimationScaling.data(), Scaling);
// Favorites
json_object *FavoritesArray = json_object_new_array();
for (uint64_t &TitleID : s_Favorites)
{
json_object *NewFavorite = json_object_new_uint64(TitleID);
json_object_array_add(FavoritesArray, NewFavorite);
}
json_object_object_add(ConfigJSON.get(), Config::Keys::Favorites.data(), FavoritesArray);
// Same but blacklist
json_object *BlacklistArray = json_object_new_array();
for (uint64_t &TitleID : s_Blacklist)
{
json_object *NewBlacklist = json_object_new_uint64(TitleID);
json_object_array_add(BlacklistArray, NewBlacklist);
}
json_object_object_add(ConfigJSON.get(), Config::Keys::BlackList.data(), BlacklistArray);
// Write config file
FsLib::File ConfigFile(CONFIG_PATH, FsOpenMode_Create | FsOpenMode_Write, std::strlen(json_object_get_string(ConfigJSON.get())));
ConfigFile << json_object_get_string(ConfigJSON.get());
}
uint8_t Config::GetByKey(std::string_view Key)
{
if (s_ConfigMap.find(Key.data()) == s_ConfigMap.end())
{
return 0;
}
return s_ConfigMap.at(Key.data());
}
FsLib::Path Config::GetWorkingDirectory(void)
{
return s_WorkingDirectory;
}
double Config::GetAnimationScaling(void)
{
return s_UIAnimationScaling;
}
void Config::AddRemoveFavorite(uint64_t TitleID)
{
auto FindTitle = std::find(s_Favorites.begin(), s_Favorites.end(), TitleID);
if (FindTitle == s_Favorites.end())
{
s_Favorites.push_back(TitleID);
}
else
{
s_Favorites.erase(FindTitle);
}
}
bool Config::IsFavorite(uint64_t TitleID)
{
if (std::find(s_Favorites.begin(), s_Favorites.end(), TitleID) == s_Favorites.end())
{
return false;
}
return true;
}
void Config::AddRemoveBlacklist(uint64_t TitleID)
{
auto FindTitle = std::find(s_Blacklist.begin(), s_Blacklist.end(), TitleID);
if (FindTitle == s_Blacklist.end())
{
s_Blacklist.push_back(TitleID);
}
else
{
s_Blacklist.erase(FindTitle);
}
}
bool Config::IsBlacklisted(uint64_t TitleID)
{
if (std::find(s_Blacklist.begin(), s_Blacklist.end(), TitleID) == s_Blacklist.end())
{
return false;
}
return true;
}

120
Source/Data/Data.cpp Normal file
View File

@@ -0,0 +1,120 @@
#include "Data/Data.hpp"
#include "Config.hpp"
#include "Data/AccountUID.hpp"
#include "FsLib.hpp"
#include "Logger.hpp"
#include "Strings.hpp"
#include <algorithm>
#include <array>
#include <switch.h>
#include <unordered_map>
#include <vector>
namespace
{
// This is easer to read imo
using UserIDPair = std::pair<AccountUid, Data::User>;
// User vector to preserve order.
std::vector<UserIDPair> s_UserVector;
// Map of Title info paired with its title/application
std::unordered_map<uint64_t, Data::TitleInfo> s_TitleInfoMap;
// Array of SaveDataSpaceIDs - SaveDataSpaceAll doesn't seem to work as it should...
constexpr std::array<FsSaveDataSpaceId, 7> s_SaveDataSpaceOrder = {FsSaveDataSpaceId_System,
FsSaveDataSpaceId_User,
FsSaveDataSpaceId_SdSystem,
FsSaveDataSpaceId_Temporary,
FsSaveDataSpaceId_SdUser,
FsSaveDataSpaceId_ProperSystem,
FsSaveDataSpaceId_SafeMode};
} // namespace
bool Data::Initialize(void)
{
// Switch can only have up to 8 accounts.
int TotalAccountsRead = 0;
AccountUid AccountIDs[8];
if (R_FAILED(accountListAllUsers(AccountIDs, 8, &TotalAccountsRead)))
{
Logger::Log("Error getting user list: 0x%X.", TotalAccountsRead);
return false;
}
// Loop through and load all users found.
for (int i = 0; i < TotalAccountsRead; i++)
{
Data::User NewUser(AccountIDs[i]);
s_UserVector.push_back(std::make_pair(AccountIDs[i], std::move(NewUser)));
}
// "System" users.
AccountUid DeviceID = {FsSaveDataType_Device};
AccountUid BCATID = {FsSaveDataType_Bcat};
AccountUid CacheID = {FsSaveDataType_Cache};
AccountUid SystemID = {FsSaveDataType_System};
s_UserVector.push_back(
std::make_pair(DeviceID, Data::User({FsSaveDataType_Device}, Strings::GetByName(Strings::Names::SaveDataTypes, 3), "Device")));
s_UserVector.push_back(
std::make_pair(BCATID, Data::User({FsSaveDataType_Bcat}, Strings::GetByName(Strings::Names::SaveDataTypes, 2), "BCAT")));
s_UserVector.push_back(
std::make_pair(CacheID, Data::User({FsSaveDataType_Cache}, Strings::GetByName(Strings::Names::SaveDataTypes, 5), "Cache")));
s_UserVector.push_back(
std::make_pair(SystemID, Data::User({FsSaveDataType_System}, Strings::GetByName(Strings::Names::SaveDataTypes, 0), "System")));
NsApplicationRecord CurrentRecord = {0};
int EntryCount = 0, EntryOffset = 0;
while (R_SUCCEEDED(nsListApplicationRecord(&CurrentRecord, 1, EntryOffset++, &EntryCount)) && EntryCount > 0)
{
s_TitleInfoMap.emplace(std::make_pair(CurrentRecord.application_id, Data::TitleInfo(CurrentRecord.application_id)));
}
for (int i = 0; i < 7; i++)
{
FsSaveDataInfo SaveInfo;
FsSaveDataInfoReader SaveInfoReader;
int64_t TotalEntries = 0;
Result FsError = fsOpenSaveDataInfoReader(&SaveInfoReader, s_SaveDataSpaceOrder[i]);
if (R_FAILED(FsError))
{
Logger::Log("Error opening save data reader with space ID %i.", i);
continue;
}
while (R_SUCCEEDED(fsSaveDataInfoReaderRead(&SaveInfoReader, &SaveInfo, 1, &TotalEntries)) && TotalEntries > 0)
{
auto FindUser = std::find_if(s_UserVector.begin(), s_UserVector.end(), [&SaveInfo](UserIDPair &IDPair) {
return IDPair.first == SaveInfo.uid;
});
// To do: Handle this right.
if (FindUser == s_UserVector.end())
{
continue;
}
// This is for if we have system save data. It has no application ID.
uint64_t ApplicationID = (SaveInfo.save_data_type == FsSaveDataType_System || SaveInfo.save_data_type == FsSaveDataType_SystemBcat)
? SaveInfo.system_save_data_id
: SaveInfo.application_id;
// Just in case.
if (s_TitleInfoMap.find(ApplicationID) == s_TitleInfoMap.end())
{
s_TitleInfoMap.emplace(std::make_pair(ApplicationID, Data::TitleInfo(ApplicationID)));
}
PdmPlayStatistics PlayStats = {0};
Result PDMError = pdmqryQueryPlayStatisticsByApplicationIdAndUserAccountId(ApplicationID, SaveInfo.uid, false, &PlayStats);
if (R_FAILED(PDMError))
{
// Logged, but not fatal.
Logger::Log("Error getting play stats for %016llX: 0x%X", ApplicationID, PDMError);
}
FindUser->second.AddToMap(SaveInfo, PlayStats);
}
}
return true;
}

63
Source/Data/TitleInfo.cpp Normal file
View File

@@ -0,0 +1,63 @@
#include "Data/TitleInfo.hpp"
#include "Colors.hpp"
#include "StringUtil.hpp"
#include <cstring>
Data::TitleInfo::TitleInfo(uint64_t ApplicationID)
{
// Used to calculate icon size.
uint64_t NsAppControlSize = 0;
// Actual control data.
NsApplicationControlData NsControlData = {0};
// Language entry
NacpLanguageEntry *LanguageEntry = nullptr;
Result NsError = nsGetApplicationControlData(NsApplicationControlSource_Storage,
ApplicationID,
&NsControlData,
sizeof(NsApplicationControlData),
&NsAppControlSize);
if (R_FAILED(NsError) || NsAppControlSize < sizeof(NsControlData.nacp))
{
std::string ApplicationIDHex = StringUtil::GetFormattedString("%04X", ApplicationID & 0xFFFF);
// Blank the nacp just to be sure.
std::memset(&m_NACP, 0x00, sizeof(NacpStruct));
// Create a place holder icon.
int TextX = 128 - (SDL::Text::GetWidth(32, ApplicationIDHex.c_str()) / 2);
m_Icon = SDL::TextureManager::CreateLoadTexture(ApplicationIDHex, 256, 256, SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET);
SDL::Text::Render(m_Icon->Get(), TextX, 112, 32, SDL::Text::NO_TEXT_WRAP, Colors::White, ApplicationIDHex.c_str());
}
else if (R_SUCCEEDED(NsError) && R_SUCCEEDED(nacpGetLanguageEntry(&NsControlData.nacp, &LanguageEntry)))
{
// Memcpy the NACP since it has all the good stuff.
std::memcpy(&m_NACP, &NsControlData.nacp, sizeof(NacpStruct));
// Load the icon.
m_Icon = SDL::TextureManager::CreateLoadTexture(LanguageEntry->name, NsControlData.icon, NsAppControlSize - sizeof(NacpStruct));
}
}
const char *Data::TitleInfo::GetTitle(void)
{
NacpLanguageEntry *Entry = nullptr;
if (R_FAILED(nacpGetLanguageEntry(&m_NACP, &Entry)))
{
return nullptr;
}
return Entry->name;
}
const char *Data::TitleInfo::GetPublisher(void)
{
NacpLanguageEntry *Entry = nullptr;
if (R_FAILED(nacpGetLanguageEntry(&m_NACP, &Entry)))
{
return nullptr;
}
return Entry->author;
}
SDL::SharedTexture Data::TitleInfo::GetIcon(void) const
{
return m_Icon;
}

117
Source/Data/User.cpp Normal file
View File

@@ -0,0 +1,117 @@
#include "Data/User.hpp"
#include "Colors.hpp"
#include "Config.hpp"
#include "Logger.hpp"
#include "SDL.hpp"
#include "StringUtil.hpp"
#include <cstring>
namespace
{
constexpr int ICON_FONT_SIZE = 42;
}
Data::User::User(AccountUid AccountID) : m_AccountID(AccountID)
{
AccountProfile Profile;
AccountProfileBase ProfileBase = {0};
// Whoever named these needs some help. What the hell?
Result ProfileError = accountGetProfile(&Profile, m_AccountID);
Result ProfileBaseError = accountProfileGet(&Profile, NULL, &ProfileBase);
if (R_FAILED(ProfileError) || R_FAILED(ProfileBaseError))
{
User::CreateAccount();
}
else
{
User::LoadAccount(Profile, ProfileBase);
}
accountProfileClose(&Profile);
}
Data::User::User(AccountUid AccountID, std::string_view Nickname, std::string_view PathSafeNickname) : m_AccountID(AccountID)
{
// Memcpy nicknames. This is actually really unsafe, but I know what's going to be copied here.
std::memcpy(m_Nickname, Nickname.data(), Nickname.length());
std::memcpy(m_PathSafeNickname, PathSafeNickname.data(), PathSafeNickname.length());
// Create Icon
int TextX = 128 - (SDL::Text::GetWidth(32, m_Nickname) / 2);
m_Icon = SDL::TextureManager::CreateLoadTexture(m_Nickname, 256, 256, SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET);
SDL::Text::Render(m_Icon->Get(), TextX, 112, 32, SDL::Text::NO_TEXT_WRAP, Colors::White, m_Nickname);
}
void Data::User::AddToMap(const FsSaveDataInfo &SaveInfo, const PdmPlayStatistics &PlayStats)
{
uint64_t ApplicationID = SaveInfo.application_id == 0 ? SaveInfo.system_save_data_id : SaveInfo.application_id;
std::memcpy(&m_UserDataMap[ApplicationID].first, &SaveInfo, sizeof(FsSaveDataInfo));
std::memcpy(&m_UserDataMap[ApplicationID].second, &PlayStats, sizeof(PdmPlayStatistics));
}
FsSaveDataInfo *Data::User::GetSaveInfoByID(uint64_t ApplicationID)
{
if (m_UserDataMap.find(ApplicationID) == m_UserDataMap.end())
{
return nullptr;
}
return &m_UserDataMap.at(ApplicationID).first;
}
PdmPlayStatistics *Data::User::GetPlayStatsByID(uint64_t ApplicationID)
{
if (m_UserDataMap.find(ApplicationID) == m_UserDataMap.end())
{
return nullptr;
}
return &m_UserDataMap.at(ApplicationID).second;
}
void Data::User::LoadAccount(AccountProfile &Profile, AccountProfileBase &ProfileBase)
{
// Try to load icon.
uint32_t IconSize = 0;
Result AccountError = accountProfileGetImageSize(&Profile, &IconSize);
if (R_FAILED(AccountError))
{
Logger::Log("Error getting user icon size: 0x%X.", AccountError);
User::CreateAccount();
return;
}
std::unique_ptr<unsigned char[]> IconBuffer(new unsigned char[IconSize]);
AccountError = accountProfileLoadImage(&Profile, IconBuffer.get(), IconSize, &IconSize);
if (R_FAILED(AccountError))
{
Logger::Log("Error loading user icon: 0x%08X.", AccountError);
User::CreateAccount();
return;
}
// We should be good at this point.
m_Icon = SDL::TextureManager::CreateLoadTexture(ProfileBase.nickname, IconBuffer.get(), IconSize);
// Memcpy the nickname.
std::memcpy(m_Nickname, &ProfileBase.nickname, 0x20);
if (!StringUtil::SanitizeStringForPath(m_Nickname, m_PathSafeNickname, 0x20))
{
std::string AccountIDString = StringUtil::GetFormattedString("Account_%08X", m_AccountID.uid[0] & 0xFFFFFFFF);
std::memcpy(m_PathSafeNickname, AccountIDString.c_str(), AccountIDString.length());
}
}
void Data::User::CreateAccount(void)
{
// This is needed a lot here.
std::string AccountIDString = StringUtil::GetFormattedString("Account_%08X", m_AccountID.uid[0] & 0xFFFFFFFF);
// Create icon
int TextX = 128 - (SDL::Text::GetWidth(32, AccountIDString.c_str()) / 2);
m_Icon = SDL::TextureManager::CreateLoadTexture(AccountIDString, 256, 256, SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET);
SDL::Text::Render(m_Icon->Get(), TextX, 112, 32, SDL::Text::NO_TEXT_WRAP, Colors::White, AccountIDString.c_str());
// Memcpy the id string for both nicknames
std::memcpy(m_Nickname, AccountIDString.c_str(), AccountIDString.length());
std::memcpy(m_PathSafeNickname, AccountIDString.c_str(), AccountIDString.length());
}

View File

@@ -1,5 +1,8 @@
#include "JKSV.hpp"
#include "AppStates/MainMenuState.hpp"
#include "Colors.hpp"
#include "Config.hpp"
#include "Data/Data.hpp"
#include "FsLib.hpp"
#include "Input.hpp"
#include "Logger.hpp"
@@ -7,8 +10,6 @@
#include "Strings.hpp"
#include <switch.h>
#include "UI/RenderFunctions.hpp"
#define ABORT_ON_FAILURE(x) \
if (!x) \
{ \
@@ -36,11 +37,15 @@ static bool InitializeService(Result (*Function)(Args...), const char *ServiceNa
JKSV::JKSV(void)
{
// FsLib
ABORT_ON_FAILURE(FsLib::Initialize());
// This doesn't really on stdio or anything.
Logger::Initialize();
// Need to init RomFS here for now until I update FsLib to take care of this.
ABORT_ON_FAILURE(InitializeService(romfsInit, "RomFS"));
// Let FsLib take care of calls to SDMC instead of fs_dev
ABORT_ON_FAILURE(FsLib::Dev::InitializeSDMC());
// SDL
ABORT_ON_FAILURE(SDL::Initialize("JKSV", 1280, 720));
ABORT_ON_FAILURE(SDL::Text::Initialize());
@@ -56,9 +61,20 @@ JKSV::JKSV(void)
ABORT_ON_FAILURE(InitializeService(setsysInitialize, "SetSys"));
ABORT_ON_FAILURE(InitializeService(socketInitializeDefault, "Socket"));
// This needs Set. JKSV also has no internal strings anymore. This is FATAL now.
// Input doesn't have anything to return.
Input::Initialize();
// Neither does config.
Config::Initialize();
// JKSV also has no internal strings anymore. This is FATAL now.
ABORT_ON_FAILURE(Strings::Initialize());
if (!Data::Initialize())
{
return;
}
// Install/setup our color changing characters.
SDL::Text::AddColorCharacter(L'#', Colors::Blue);
SDL::Text::AddColorCharacter(L'*', Colors::Red);
@@ -71,7 +87,8 @@ JKSV::JKSV(void)
// This can't be in an initializer list because it needs SDL initialized.
m_HeaderIcon = SDL::TextureManager::CreateLoadTexture("HeaderIcon", "romfs:/Textures/HeaderIcon.png");
Input::Initialize();
// Push initial main menu state.
JKSV::PushState(std::make_shared<MainMenuState>());
m_IsRunning = true;
}
@@ -104,6 +121,16 @@ void JKSV::Update(void)
{
m_IsRunning = false;
}
if (!m_StateVector.empty())
{
while (!m_StateVector.back()->IsActive())
{
m_StateVector.pop_back();
m_StateVector.back()->GiveFocus();
}
m_StateVector.back()->Update();
}
}
void JKSV::Render(void)
@@ -121,21 +148,33 @@ void JKSV::Render(void)
{
SDL::Text::Render(NULL,
8,
682,
12,
680,
14,
SDL::Text::NO_TEXT_WRAP,
Colors::White,
Strings::GetByName(Strings::Names::TranslationInfo, 0),
Strings::GetByName(Strings::Names::TranslationInfo, 1));
}
SDL::Text::Render(NULL, 8, 700, 12, SDL::Text::NO_TEXT_WRAP, Colors::White, "v %02d.%02d.%04d", BUILD_MON, BUILD_DAY, BUILD_YEAR);
UI::RenderDialogBox(NULL, 320, 240, 320, 240);
if (!m_StateVector.empty())
{
for (auto &CurrentState : m_StateVector)
{
CurrentState->Render();
}
}
SDL::Text::Render(NULL, 8, 700, 14, SDL::Text::NO_TEXT_WRAP, Colors::White, "v. %02d.%02d.%04d", BUILD_MON, BUILD_DAY, BUILD_YEAR);
SDL::FrameEnd();
}
void JKSV::PushState(std::shared_ptr<AppState> NewState)
{
if (!m_StateVector.empty())
{
m_StateVector.back()->TakeFocus();
}
NewState->GiveFocus();
m_StateVector.push_back(NewState);
}

View File

@@ -1,10 +1,18 @@
#include "StringUtil.hpp"
#include <algorithm>
#include <array>
#include <cstdarg>
#include <cstring>
#include <switch.h>
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<uint32_t, 13> FORBIDDEN_PATH_CHARACTERS =
{L',', L'/', L'\\', L'<', L'>', L':', L'"', L'|', L'?', L'*', L'', L'©', L'®'};
} // namespace
std::string StringUtil::GetFormattedString(const char *Format, ...)
{
@@ -17,3 +25,57 @@ std::string StringUtil::GetFormattedString(const char *Format, ...)
return std::string(VaBuffer);
}
void StringUtil::ReplaceInString(std::string &Target, std::string_view Find, std::string_view Replace)
{
size_t StringPosition = 0;
while ((StringPosition = Target.find(Find, StringPosition)) != Target.npos)
{
Target.replace(StringPosition, Find.length(), Replace);
}
}
bool StringUtil::SanitizeStringForPath(const char *StringIn, char *StringOut, size_t StringOutSize)
{
uint32_t Codepoint = 0;
size_t StringLength = std::strlen(StringIn);
for (size_t i = 0, StringOutOffset = 0; i < StringLength;)
{
ssize_t UnitCount = decode_utf8(&Codepoint, reinterpret_cast<const uint8_t *>(&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<size_t>(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;
}

View File

@@ -1,6 +1,7 @@
#include "Strings.hpp"
#include "FsLib.hpp"
#include "JSON.hpp"
#include "StringUtil.hpp"
#include <map>
#include <string>
#include <unordered_map>
@@ -30,6 +31,7 @@ namespace
{SetLanguage_PTBR, "PTBR.json"}};
} // namespace
// This returns the language file to use depending on the system's language.
static FsLib::Path GetStringFilePath(void)
{
FsLib::Path ReturnPath = "romfs:/Text";
@@ -50,6 +52,27 @@ static FsLib::Path GetStringFilePath(void)
return ReturnPath / s_FileMap.at(Language);
}
static void ReplaceButtonsInString(std::string &Target)
{
StringUtil::ReplaceInString(Target, "[A]", "\ue0e0");
StringUtil::ReplaceInString(Target, "[B]", "\ue0e1");
StringUtil::ReplaceInString(Target, "[X]", "\ue0e2");
StringUtil::ReplaceInString(Target, "[Y]", "\ue0e3");
StringUtil::ReplaceInString(Target, "[L]", "\ue0e4");
StringUtil::ReplaceInString(Target, "[R]", "\ue0e5");
StringUtil::ReplaceInString(Target, "[ZL]", "\ue0e6");
StringUtil::ReplaceInString(Target, "[ZR]", "\ue0e7");
StringUtil::ReplaceInString(Target, "[SL]", "\ue0e8");
StringUtil::ReplaceInString(Target, "[SR]", "\ue0e9");
StringUtil::ReplaceInString(Target, "[DPAD]", "\ue0ea");
StringUtil::ReplaceInString(Target, "[DUP]", "\ue0eb");
StringUtil::ReplaceInString(Target, "[DDOWN]", "\ue0ec");
StringUtil::ReplaceInString(Target, "[DLEFT]", "\ue0ed");
StringUtil::ReplaceInString(Target, "[DRIGHT]", "\ue0ee");
StringUtil::ReplaceInString(Target, "[+]", "\ue0ef");
StringUtil::ReplaceInString(Target, "[-]", "\ue0f0");
}
bool Strings::Initialize()
{
FsLib::Path StringsPath = GetStringFilePath();
@@ -77,6 +100,13 @@ bool Strings::Initialize()
}
json_object_iter_next(&StringIterator);
}
// Loop through entire map and replace the buttons.
for (auto &[Key, String] : s_StringMap)
{
ReplaceButtonsInString(String);
}
return true;
}

18
Source/UI/ColorMod.cpp Normal file
View File

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

124
Source/UI/Menu.cpp Normal file
View File

@@ -0,0 +1,124 @@
#include "UI/Menu.hpp"
#include "Colors.hpp"
#include "Config.hpp"
#include "Input.hpp"
#include "UI/RenderFunctions.hpp"
UI::Menu::Menu(int X, int Y, int Width, int FontSize, int ScrollLength)
: m_X(X), m_Y(Y), m_OriginalY(Y), m_FontSize(FontSize), m_ScrollLength(ScrollLength), m_MenuRenderLength(ScrollLength * 2), m_Width(Width),
m_Height(FontSize + 32)
{
static int MenuID = 0;
// Create target for menu option
std::string MenuTargetName = "Menu_" + std::to_string(MenuID++);
m_OptionTarget =
SDL::TextureManager::CreateLoadTexture(MenuTargetName, m_Width, m_Height, SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET);
}
void UI::Menu::Update(void)
{
// Bail if there's nothing to update.
if (m_Options.empty())
{
return;
}
if (Input::ButtonPressed(HidNpadButton_AnyUp) && --m_Selected < 0)
{
m_Selected = m_OptionsLength;
}
else if (Input::ButtonPressed(HidNpadButton_AnyDown) && ++m_Selected > m_OptionsLength)
{
m_Selected = 0;
}
else if (Input::ButtonPressed(HidNpadButton_Left) && (m_Selected -= m_ScrollLength) < 0)
{
m_Selected = 0;
}
else if (Input::ButtonPressed(HidNpadButton_AnyRight) && (m_Selected += m_ScrollLength) > m_OptionsLength)
{
m_Selected = m_OptionsLength;
}
// Calculate scrolling
if (m_Selected < m_ScrollLength)
{
m_TargetY = m_OriginalY;
}
else if (m_Selected >= m_ScrollLength && m_OptionsLength > m_MenuRenderLength)
{
m_TargetY = m_OriginalY + -(m_Height * (m_OptionsLength * m_MenuRenderLength));
}
else if (m_Selected > m_MenuRenderLength && m_Selected < (m_OptionsLength - m_MenuRenderLength))
{
m_TargetY = -(m_Height * (m_Selected - m_MenuRenderLength));
}
if (m_Y != m_TargetY)
{
m_Y += std::ceil((m_TargetY - m_Y) / Config::GetAnimationScaling());
}
}
void UI::Menu::Render(SDL_Texture *Target)
{
if (m_Options.empty())
{
return;
}
m_ColorMod.Update();
// I hate doing this.
int TargetHeight = 0;
SDL_QueryTexture(Target, NULL, NULL, NULL, &TargetHeight);
for (int i = 0, TempY = m_Y; i < m_OptionsLength; i++, TempY += m_Height)
{
if (TempY < 0 || TempY > TargetHeight)
{
continue;
}
// Clear target texture.
m_OptionTarget->Clear(Colors::Transparent);
if (i == m_Selected)
{
// Render the bounding box
UI::RenderBoundingBox(Target, m_X - 4, m_Y - 4, m_Width + 8, m_Height + 8, m_ColorMod);
// Render the little rectangle.
SDL::RenderRectFill(m_OptionTarget->Get(), 8, 2, 4, m_Height - 4, {0x00FFC5FF});
}
// Render text to target.
SDL::Text::Render(m_OptionTarget->Get(),
14,
(m_Height / 2) - (m_FontSize / 2),
m_FontSize,
SDL::Text::NO_TEXT_WRAP,
i == m_Selected ? Colors::Blue : Colors::White,
m_Options.at(i).c_str());
// Render target to target
m_OptionTarget->Render(Target, m_X, TempY);
}
}
void UI::Menu::AddOption(std::string_view NewOption)
{
m_Options.push_back(NewOption.data());
++m_OptionsLength;
}
int UI::Menu::GetSelected(void) const
{
return m_Selected;
}
void UI::Menu::SetSelected(int Selected)
{
m_Selected = Selected;
}
void UI::Menu::Reset(void)
{
m_OptionsLength = -1;
m_Options.clear();
}

View File

@@ -25,3 +25,29 @@ void UI::RenderDialogBox(SDL_Texture *Target, int X, int Y, int Width, int Heigh
SDL::RenderRectFill(NULL, X + 16, (Y + Height) - 16, Width - 32, 16, Colors::DialogBox);
s_DialogCorners->RenderPart(NULL, (X + Width) - 16, (Y + Height) - 16, 16, 16, 16, 16);
}
void UI::RenderBoundingBox(SDL_Texture *Target, int X, int Y, int Width, int Height, uint8_t ColorMod)
{
if (!s_MenuBoundingCorners)
{
s_MenuBoundingCorners = SDL::TextureManager::CreateLoadTexture("MenuBoundingCorners", "romfs:/Textures/MenuBounding.png");
}
// Setup color.
SDL::Color RenderMod = {(0x88 + ColorMod) << 16 | (0xC5 + (ColorMod / 2) << 8) | 0xFF};
// This shouldn't fail, but I don't really care if it does.
SDL_SetTextureColorMod(s_MenuBoundingCorners->Get(), RenderMod.RGBA[3], RenderMod.RGBA[2], RenderMod.RGBA[1]);
// Top
s_MenuBoundingCorners->RenderPart(Target, X, Y, 0, 0, 8, 8);
SDL::RenderRectFill(Target, X + 8, Y, Width - 16, 4, RenderMod);
s_MenuBoundingCorners->RenderPart(Target, (X + Width) - 8, Y, 8, 0, 8, 8);
// Middle
SDL::RenderRectFill(Target, X, Y + 8, 4, Height - 16, RenderMod);
SDL::RenderRectFill(Target, (X + Width) - 4, Y + 8, 4, Height - 16, RenderMod);
// Bottom
s_MenuBoundingCorners->RenderPart(Target, X, (Y + Height) - 8, 0, 8, 8, 8);
SDL::RenderRectFill(Target, X + 8, (Y + Height) - 4, Width - 16, 4, RenderMod);
s_MenuBoundingCorners->RenderPart(Target, (X + Width) - 8, (Y + Height) - 8, 8, 8, 8, 8);
}