Implement/complete TitleInfoState

This commit is contained in:
J-D-K
2025-06-12 16:25:22 -04:00
parent 95ecbec3e4
commit 8dd16c12bc
12 changed files with 289 additions and 60 deletions

View File

@@ -6,6 +6,7 @@
#include "system/Timer.hpp"
#include "ui/Menu.hpp"
#include "ui/SlideOutPanel.hpp"
#include "ui/TextScroll.hpp"
#include <memory>
/// @brief This is the state where the user can backup and restore saves.
@@ -49,6 +50,9 @@ class BackupMenuState : public AppState
/// @brief Directory listing of the above.
fslib::Directory m_directoryListing;
/// @brief This is the scrolling text at the top.
ui::TextScroll m_titleScroll;
/// @brief Variable that saves whether or not the filesystem has data in it.
bool m_saveHasData = false;

View File

@@ -34,12 +34,36 @@ class TitleInfoState : public AppState
/// @brief This is a pointer to the title's icon.
sdl::SharedTexture m_icon;
/// @brief This is the scrolling text for the title.
ui::TextScroll m_titleScroll;
/// @brief This is the scrolling text for the publisher.
ui::TextScroll m_publisherScroll;
/// @brief This string holds the application ID.
std::string m_applicationID;
/// @brief This holds the hex save data id of the file on nand.
std::string m_saveDataID;
/// @brief This holds the play time string.
std::string m_playTime;
/// @brief This holds the total launches string.
std::string m_totalLaunches;
/// @brief This holds the save data type string.
std::string m_saveDataType;
/// @brief Bool to tell whether or not static members are initialized.
static inline bool sm_initialized = false;
/// @brief This is the render target for the title text just in case it needs scrolling.
static inline sdl::SharedTexture sm_titleTarget = nullptr;
/// @brief This is the render target for the publisher string.
static inline sdl::SharedTexture sm_publisherTarget = nullptr;
/// @brief Slide panel.
static inline std::unique_ptr<ui::SlideOutPanel> sm_slidePanel = nullptr;
};

View File

@@ -33,6 +33,7 @@ namespace strings
static constexpr std::string_view TITLE_OPTION_STATUS = "TitleOptionStatus";
static constexpr std::string_view TITLE_OPTION_POPS = "TitleOptionPops";
static constexpr std::string_view TITLE_OPTION_CONFIRMATIONS = "TitleOptionConfirmations";
static constexpr std::string_view TITLE_INFO_STRINGS = "TitleInfo";
static constexpr std::string_view POP_MESSAGES_GENERAL = "PopMessagesGeneral";
static constexpr std::string_view POP_MESSAGES_BACKUP_MENU = "PopMessagesBackupMenu";
static constexpr std::string_view POP_MESSAGES_SAVE_CREATE = "PopMessagesSaveCreate";

View File

@@ -15,11 +15,6 @@ namespace sys
/// @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);

View File

@@ -12,18 +12,26 @@ namespace ui
/// @brief This is only here so I can get around the backup menu having static members.
TextScroll(void) = default;
/// @brief Constructs a new TextScroll.
/// @param text Text to be scrolled.
/// @param availableWidth Maximum width. If this is not exceeded,text is just rendered centered according to it.
TextScroll(std::string_view text, int fontSize, int availableWidth, int y, sdl::Color color);
/// @brief Constructor for TextScroll.
/// @param text Text to create the textscroll with.
/// @param fontSize Size of the font in pixels to use.
/// @param availableWidth Available width of the render target.
/// @param y Y coordinate to render to.
/// @param center Whether or not text should be centered if it's not wide enough for scrolling.
/// @param color Color to use to render the text.
TextScroll(std::string_view text, int fontSize, int availableWidth, int y, bool center, sdl::Color color);
/// @brief Required destructor.
~TextScroll() {};
/// @brief Same as the default constructor. To do: Figure this out better sometime.
/// @param textScroll TextScroll to copy.
/// @return Reference to itself?
TextScroll &operator=(const TextScroll &textScroll);
/// @brief Creates/sets the text and parameters for TextScroll.
/// @param text Text to display/scroll.
/// @param fontSize Font size to use when rendering.
/// @param availableWidth Available width of the target buffer.
/// @param y Y coordinate used to render text.
/// @param center Whether or not text should be centered if it's not wide enough for scrolling.
/// @param color Color to use to render text.
void create(std::string_view text, int fontSize, int availableWidth, int y, bool center, sdl::Color color);
/// @brief Runs the update routine.
/// @param hasFocus Whether or not the calling state has focus.

View File

@@ -159,6 +159,13 @@
"Are you sure you would like to reset the save data for #%s#? *This will delete the current save data for the title as if it were never run!*",
"Are you sure you want to delete `%s`'s save data for #%s#? This will permanently delete it from the system."
],
"TitleInfo": [
"App ID: %016lX",
"Save ID: %016lx",
"Play Time: %02d:%02d:%02d",
"Launches: %i",
"Save Type: %s"
],
"PopMessagesGeneral": [
"Unable to exit JKSV while tasks are running!"
],

View File

@@ -75,7 +75,7 @@ BackupMenuState::BackupMenuState(data::User *user, data::TitleInfo *titleInfo, F
stringutil::get_formatted_string("`%s` - %s", m_user->get_nickname(), m_titleInfo->get_title());
// This needs sm_panelWidth or it'd be in the initializer list.
sm_slidePanel->push_new_element(std::make_shared<ui::TextScroll>(panelString, 22, sm_panelWidth, 8, colors::WHITE));
m_titleScroll.create(panelString, 22, sm_panelWidth, 8, true, colors::WHITE);
fslib::Directory saveCheck(fs::DEFAULT_SAVE_ROOT);
m_saveHasData = saveCheck.get_count() > 0;
@@ -90,6 +90,8 @@ BackupMenuState::~BackupMenuState()
void BackupMenuState::update(void)
{
bool hasFocus = AppState::has_focus();
if (input::button_pressed(HidNpadButton_A) && sm_backupMenu->get_selected() == 0 && m_saveHasData)
{
// get name for backup.
@@ -230,20 +232,29 @@ void BackupMenuState::update(void)
AppState::deactivate();
}
// Update title scrolling.
m_titleScroll.update(hasFocus);
// Update panel.
sm_slidePanel->update(AppState::has_focus());
sm_slidePanel->update(hasFocus);
// This state bypasses the Slideout panel's normal behavior because it kind of has to.
sm_backupMenu->update(AppState::has_focus());
sm_backupMenu->update(hasFocus);
}
void BackupMenuState::render(void)
{
// Save this locally.
bool hasFocus = AppState::has_focus();
// Clear panel target.
sm_slidePanel->clear_target();
// Grab the render target.
SDL_Texture *slideTarget = sm_slidePanel->get_target();
// Start with [Name] - [Title]
m_titleScroll.render(slideTarget, hasFocus);
sdl::render_line(slideTarget, 10, 42, sm_panelWidth - 10, 42, colors::WHITE);
sdl::render_line(slideTarget, 10, 648, sm_panelWidth - 10, 648, colors::WHITE);
sdl::text::render(slideTarget,
@@ -257,11 +268,11 @@ void BackupMenuState::render(void)
// Clear menu target.
sm_menuRenderTarget->clear(colors::TRANSPARENT);
// render menu to it.
sm_backupMenu->render(sm_menuRenderTarget->get(), AppState::has_focus());
sm_backupMenu->render(sm_menuRenderTarget->get(), hasFocus);
// render it to panel target.
sm_menuRenderTarget->render(slideTarget, 0, 43);
sm_slidePanel->render(NULL, AppState::has_focus());
sm_slidePanel->render(NULL, hasFocus);
}
void BackupMenuState::refresh(void)

View File

@@ -2,11 +2,22 @@
#include "colors.hpp"
#include "input.hpp"
#include "sdl.hpp"
#include "strings.hpp"
#include "stringutil.hpp"
namespace
{
/// @brief This is the width of the panel in pixels.
constexpr int SIZE_PANEL_WIDTH = 480;
/// @brief This is the gap on both sides
constexpr int SIZE_PANEL_SUB = 16;
/// @brief This is the size used for rendering text here.
constexpr int SIZE_FONT = 24;
/// @brief This is the vertical size in pixels of the render targets for title/publisher.
constexpr int SIZE_TEXT_TARGET_HEIGHT = 40;
} // namespace
TitleInfoState::TitleInfoState(data::User *user, data::TitleInfo *titleInfo)
@@ -17,12 +28,59 @@ TitleInfoState::TitleInfoState(data::User *user, data::TitleInfo *titleInfo)
{
// This is the slide panel everything is rendered to.
sm_slidePanel = std::make_unique<ui::SlideOutPanel>(SIZE_PANEL_WIDTH, ui::SlideOutPanel::Side::Right);
// This is the render target for the title.
sm_titleTarget = sdl::TextureManager::create_load_texture("infoTitleTarget",
SIZE_PANEL_WIDTH - 32,
32,
SIZE_PANEL_WIDTH - SIZE_PANEL_SUB,
SIZE_TEXT_TARGET_HEIGHT,
SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET);
// This is the render target for the publisher.
sm_publisherTarget =
sdl::TextureManager::create_load_texture("infoPublisherTarget",
SIZE_PANEL_WIDTH - SIZE_PANEL_SUB,
SIZE_TEXT_TARGET_HEIGHT,
SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET);
sm_initialized = true;
}
// Do this instead of calling the function everytime.
uint64_t applicationID = m_titleInfo->get_application_id();
// These is needed at some point.
FsSaveDataInfo *saveInfo = m_user->get_save_info_by_id(applicationID);
PdmPlayStatistics *playStats = m_user->get_play_stats_by_id(applicationID);
// Title text.
m_titleScroll
.create(m_titleInfo->get_title(), SIZE_FONT, SIZE_PANEL_WIDTH - SIZE_PANEL_SUB, 6, false, colors::WHITE);
// Publisher.
m_publisherScroll
.create(m_titleInfo->get_publisher(), SIZE_FONT, SIZE_PANEL_WIDTH - SIZE_PANEL_SUB, 6, false, colors::WHITE);
// Grab the application ID string.
m_applicationID =
stringutil::get_formatted_string(strings::get_by_name(strings::names::TITLE_INFO_STRINGS, 0), applicationID);
// Same here. I think these use lower case characters on the NAND?
m_saveDataID = stringutil::get_formatted_string(strings::get_by_name(strings::names::TITLE_INFO_STRINGS, 1),
saveInfo->save_data_id);
// This is simple.
m_totalLaunches = stringutil::get_formatted_string(strings::get_by_name(strings::names::TITLE_INFO_STRINGS, 3),
playStats->total_launches);
// This should be semi-simple.
m_saveDataType = stringutil::get_formatted_string(
strings::get_by_name(strings::names::TITLE_INFO_STRINGS, 4),
strings::get_by_name(strings::names::SAVE_DATA_TYPES, saveInfo->save_data_type));
// Calculate play time. We're going to use non-floating point to truncate the remainders.
int64_t seconds = playStats->playtime / static_cast<int64_t>(1e+9);
int64_t hours = seconds / 3600;
int64_t minutes = (seconds % 3600) / 60;
seconds %= 60;
m_playTime = stringutil::get_formatted_string(strings::get_by_name(strings::names::TITLE_INFO_STRINGS, 2),
hours,
minutes,
seconds);
}
TitleInfoState::~TitleInfoState()
@@ -33,8 +91,15 @@ TitleInfoState::~TitleInfoState()
void TitleInfoState::update(void)
{
// Grab this instead of calling the function over and over.
bool hasFocus = AppState::has_focus();
// Update slide panel.
sm_slidePanel->update(AppState::has_focus());
sm_slidePanel->update(hasFocus);
// Update the scrolling text.
m_titleScroll.update(hasFocus);
m_publisherScroll.update(hasFocus);
if (input::button_pressed(HidNpadButton_B))
{
@@ -49,18 +114,110 @@ void TitleInfoState::update(void)
void TitleInfoState::render(void)
{
// This is the vertical gap for rendering text.
static constexpr int SIZE_VERT_GAP = SIZE_TEXT_TARGET_HEIGHT + 4;
// This is the size to render the rectangles at.
static constexpr int SIZE_RECT_WIDTH = SIZE_PANEL_WIDTH - SIZE_PANEL_SUB;
// This is whether or not the state currently has focus.
bool hasFocus = AppState::has_focus();
// Clear the title and publisher targets.
sm_titleTarget->clear(colors::DIALOG_BOX);
sm_publisherTarget->clear(colors::CLEAR_COLOR);
// Grab the panel's target and clear it. To do: This how I originally intended to.
sm_slidePanel->clear_target();
// Grab the panel target for rendering to.
SDL_Texture *panelTarget = sm_slidePanel->get_target();
// This is our Y rendering coordinate. It's easier to adjust this as needed than change everything.
// This starts under the icon.
int y = 304;
// Start by rendering the icon.
m_icon->render(panelTarget, 112, 24);
m_icon->render_stretched(panelTarget, 80, 8, 320, 320);
// Clear the title target and render the text scroll to it.
sm_titleTarget->clear(colors::DIALOG_BOX);
// Render the textscroll to the target, then target to the panel target.
m_titleScroll.render(sm_titleTarget->get(), hasFocus);
sm_titleTarget->render(panelTarget, 8, (y += SIZE_VERT_GAP));
// Publisher.
m_publisherScroll.render(sm_publisherTarget->get(), hasFocus);
sm_publisherTarget->render(panelTarget, 8, (y += SIZE_VERT_GAP));
// These are different since the probably won't go outside the bounds of the rectangle.
sdl::render_rect_fill(panelTarget,
8,
(y += SIZE_VERT_GAP),
SIZE_RECT_WIDTH,
SIZE_TEXT_TARGET_HEIGHT,
colors::DIALOG_BOX);
// Text needs to be aligned like the scrolling text.
sdl::text::render(panelTarget,
16,
y + 6,
SIZE_FONT,
sdl::text::NO_TEXT_WRAP,
colors::WHITE,
m_applicationID.c_str());
// These are different since the probably won't go outside the bounds of the rectangle.
sdl::render_rect_fill(panelTarget,
8,
(y += SIZE_VERT_GAP),
SIZE_RECT_WIDTH,
SIZE_TEXT_TARGET_HEIGHT,
colors::CLEAR_COLOR);
// Text needs to be aligned like the scrolling text.
sdl::text::render(panelTarget, 16, y + 6, SIZE_FONT, sdl::text::NO_TEXT_WRAP, colors::WHITE, m_saveDataID.c_str());
// These are different since the probably won't go outside the bounds of the rectangle.
sdl::render_rect_fill(panelTarget,
8,
(y += SIZE_VERT_GAP),
SIZE_RECT_WIDTH,
SIZE_TEXT_TARGET_HEIGHT,
colors::DIALOG_BOX);
// Text needs to be aligned like the scrolling text.
sdl::text::render(panelTarget, 16, y + 6, SIZE_FONT, sdl::text::NO_TEXT_WRAP, colors::WHITE, m_playTime.c_str());
// These are different since the probably won't go outside the bounds of the rectangle.
sdl::render_rect_fill(panelTarget,
8,
(y += SIZE_VERT_GAP),
SIZE_RECT_WIDTH,
SIZE_TEXT_TARGET_HEIGHT,
colors::CLEAR_COLOR);
// Text needs to be aligned like the scrolling text.
sdl::text::render(panelTarget,
16,
y + 6,
SIZE_FONT,
sdl::text::NO_TEXT_WRAP,
colors::WHITE,
m_totalLaunches.c_str());
// These are different since the probably won't go outside the bounds of the rectangle.
sdl::render_rect_fill(panelTarget,
8,
(y += SIZE_VERT_GAP),
SIZE_RECT_WIDTH,
SIZE_TEXT_TARGET_HEIGHT,
colors::DIALOG_BOX);
// Text needs to be aligned like the scrolling text.
sdl::text::render(panelTarget,
16,
y + 6,
SIZE_FONT,
sdl::text::NO_TEXT_WRAP,
colors::WHITE,
m_saveDataType.c_str());
sm_slidePanel->render(NULL, AppState::has_focus());
}

View File

@@ -21,9 +21,6 @@ data::TitleInfo::TitleInfo(uint64_t applicationID) : m_applicationID(application
if (R_FAILED(nsError) || nsAppControlSize < sizeof(m_data.nacp))
{
// This should be false by default, but just in case.
m_hasData = false;
// This is the lowest four hex values of the title.
std::string applicationIDHex = stringutil::get_formatted_string("%04X", m_applicationID & 0xFFFF);

View File

@@ -342,7 +342,7 @@ static void load_save_data_info(void)
default:
{
// Default is just the ID in the save info struct.
// Default is just the ID in the save info struct. This should be fine for system saves too.
accountID = saveInfo.uid;
}
break;
@@ -383,9 +383,10 @@ static void load_save_data_info(void)
// I feel weird allcating space for this even if it's not used, but whatever.
PdmPlayStatistics stats = {0};
// This should be an OKish way to filter out system titles...
if (titleInfo.has_control_data() &&
R_FAILED(
pdmqryQueryPlayStatisticsByApplicationIdAndUserAccountId(applicationID, accountID, false, &stats)))
if (R_FAILED(pdmqryQueryPlayStatisticsByApplicationIdAndUserAccountId(applicationID,
saveInfo.uid,
false,
&stats)))
{
// This isn't fatal.
logger::log("Error getting play stats for title %016llX!", applicationID);

View File

@@ -1,21 +1,20 @@
#include "system/Timer.hpp"
#include <SDL2/SDL.h>
#include "logger.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();
// Start by recording trigger ticks.
m_triggerTicks = triggerTicks;
// Get the current tick count.
m_startingTicks = SDL_GetTicks64();
}
bool sys::Timer::is_triggered(void)
@@ -27,8 +26,10 @@ bool sys::Timer::is_triggered(void)
{
return false;
}
// Reset starting ticks.
m_startingTicks = currentTicks;
// Trigger me timbers~
return true;
}

View File

@@ -1,37 +1,59 @@
#include "ui/TextScroll.hpp"
#include "sdl.hpp"
ui::TextScroll::TextScroll(std::string_view text, int fontSize, int availableWidth, int y, sdl::Color color)
: m_text(text.data()), m_y(y), m_fontSize(fontSize), m_textColor(color), m_scrollTimer(3000)
{
// Grab text width first.
m_textWidth = sdl::text::get_width(m_fontSize, m_text.c_str());
#include "logger.hpp"
// Check if text needs scrolling for width provided.
namespace
{
/// @brief This is the number of ticks needed before the text starts scrolling.
constexpr uint64_t TICKS_SCROLL_TRIGGER = 3000;
} // namespace
ui::TextScroll::TextScroll(std::string_view text,
int fontSize,
int availableWidth,
int y,
bool center,
sdl::Color color)
{
TextScroll::create(text, fontSize, availableWidth, y, center, color);
}
void ui::TextScroll::create(std::string_view text,
int fontSize,
int availableWidth,
int y,
bool center,
sdl::Color color)
{
// Copy the text and stuff.
m_text = text;
m_y = y;
m_fontSize = fontSize;
m_textColor = color;
m_scrollTimer.start(TICKS_SCROLL_TRIGGER);
// Get the width and calculate X.
m_textWidth = sdl::text::get_width(m_fontSize, m_text.c_str());
if (m_textWidth > availableWidth)
{
// Set the X coordinate to 8 and make sure this knows it needs to scroll.
m_x = 8;
m_textScrolling = true;
logger::log("Scrolling needed?");
}
else
else if (center)
{
// Just center it.
m_x = (availableWidth / 2) - (m_textWidth / 2);
logger::log("Centered.");
}
else
{
// Just set this to 8. To do: Figure out how to make this cleaner.
m_x = 8;
logger::log("Aligned.");
}
}
ui::TextScroll &ui::TextScroll::operator=(const ui::TextScroll &textScroll)
{
m_text = textScroll.m_text;
m_x = textScroll.m_x;
m_y = textScroll.m_y;
m_fontSize = textScroll.m_fontSize;
m_textColor = textScroll.m_textColor;
m_textWidth = textScroll.m_textWidth;
m_textScrolling = textScroll.m_textScrolling;
m_textScrollTriggered = textScroll.m_textScrollTriggered;
m_scrollTimer = textScroll.m_scrollTimer;
return *this;
}
void ui::TextScroll::update(bool hasFocus)
@@ -39,6 +61,7 @@ void ui::TextScroll::update(bool hasFocus)
// I don't think needs to care about having focus.
if (m_textScrolling && m_scrollTimer.is_triggered())
{
logger::log("Text scroll triggered?");
m_x -= 2;
m_textScrollTriggered = true;
}