mirror of
https://github.com/J-D-K/JKSV.git
synced 2026-08-28 13:34:05 -05:00
Data partially implemented with no crashes. Getting too tired.
This commit is contained in:
@@ -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
|
||||
{
|
||||
|
||||
23
Include/AppStates/MainMenuState.hpp
Normal file
23
Include/AppStates/MainMenuState.hpp
Normal 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;
|
||||
};
|
||||
@@ -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
52
Include/Config.hpp
Normal 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
|
||||
12
Include/Data/AccountUID.hpp
Normal file
12
Include/Data/AccountUID.hpp
Normal 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]);
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
#pragma once
|
||||
#include "Data/AccountUID.hpp"
|
||||
#include "Data/TitleInfo.hpp"
|
||||
#include "Data/User.hpp"
|
||||
|
||||
namespace Data
|
||||
{
|
||||
|
||||
28
Include/Data/TitleInfo.hpp
Normal file
28
Include/Data/TitleInfo.hpp
Normal 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
41
Include/Data/User.hpp
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
23
Include/UI/ColorMod.hpp
Normal 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
15
Include/UI/Element.hpp
Normal 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
14
Include/UI/IconMenu.hpp
Normal 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
53
Include/UI/Menu.hpp
Normal 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
|
||||
@@ -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
|
||||
|
||||
2
Makefile
2
Makefile
@@ -32,7 +32,7 @@ include $(DEVKITPRO)/libnx/switch_rules
|
||||
#---------------------------------------------------------------------------------
|
||||
TARGET := JKSV
|
||||
BUILD := build
|
||||
SOURCES := Source Source/AppStates Source/UI
|
||||
SOURCES := Source Source/AppStates Source/UI Source/Data
|
||||
DATA := data
|
||||
INCLUDES := Include ./Libraries/FsLib/Switch/FsLib/include ./Libraries/SDLLib/SDL/include
|
||||
EXEFS_SRC := exefs_src
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
BIN
RomFS/Textures/MenuBackground.png
Normal file
BIN
RomFS/Textures/MenuBackground.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 66 KiB |
28
Source/AppStates/MainMenuState.cpp
Normal file
28
Source/AppStates/MainMenuState.cpp
Normal 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
217
Source/Config.cpp
Normal 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
120
Source/Data/Data.cpp
Normal 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
63
Source/Data/TitleInfo.cpp
Normal 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
117
Source/Data/User.cpp
Normal 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());
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
18
Source/UI/ColorMod.cpp
Normal 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
124
Source/UI/Menu.cpp
Normal 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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user