Atmosphere/troposphere/daybreak/source/ui.cpp

1325 lines
52 KiB
C++

/*
* Copyright (c) Adubbz
*
* This program is free software; you can redistribute it and/or modify it
* under the terms and conditions of the GNU General Public License,
* version 2, as published by the Free Software Foundation.
*
* This program is distributed in the hope it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <algorithm>
#include <cstdarg>
#include <cstdio>
#include <cstring>
#include <limits>
#include <dirent.h>
#include "ui.hpp"
#include "ui_util.hpp"
#include "assert.hpp"
namespace dbk {
namespace {
static constexpr u32 ExosphereApiVersionConfigItem = 65000;
static constexpr u32 ExosphereHasRcmBugPatch = 65004;
static constexpr u32 ExosphereEmummcType = 65007;
static constexpr u32 ExosphereSupportedHosVersion = 65011;
/* Insets of content within windows. */
static constexpr float HorizontalInset = 20.0f;
static constexpr float BottomInset = 20.0f;
/* Insets of content within text areas. */
static constexpr float TextHorizontalInset = 8.0f;
static constexpr float TextVerticalInset = 8.0f;
static constexpr float ButtonHeight = 60.0f;
static constexpr float ButtonHorizontalGap = 10.0f;
static constexpr float VerticalGap = 10.0f;
u32 g_screen_width;
u32 g_screen_height;
constinit u32 g_supported_version = std::numeric_limits<u32>::max();
std::shared_ptr<Menu> g_current_menu;
bool g_initialized = false;
bool g_exit_requested = false;
PadState g_pad;
u32 g_prev_touch_count = -1;
HidTouchScreenState g_start_touch;
bool g_started_touching = false;
bool g_tapping = false;
bool g_touches_moving = false;
bool g_finished_touching = false;
/* Update install state. */
char g_update_path[FS_MAX_PATH];
bool g_reset_to_factory = false;
bool g_exfat_supported = false;
bool g_use_exfat = false;
constexpr u32 MaxTapMovement = 20;
void UpdateInput() {
/* Scan for input and update touch state. */
padUpdate(&g_pad);
HidTouchScreenState current_touch;
hidGetTouchScreenStates(&current_touch, 1);
const u32 touch_count = current_touch.count;
if (g_prev_touch_count == 0 && touch_count > 0) {
hidGetTouchScreenStates(&g_start_touch, 1);
g_started_touching = true;
g_tapping = true;
} else {
g_started_touching = false;
}
if (g_prev_touch_count > 0 && touch_count == 0) {
g_finished_touching = true;
g_tapping = false;
} else {
g_finished_touching = false;
}
/* Check if currently moving. */
if (g_prev_touch_count > 0 && touch_count > 0) {
if ((abs(current_touch.touches[0].x - g_start_touch.touches[0].x) > MaxTapMovement || abs(current_touch.touches[0].y - g_start_touch.touches[0].y) > MaxTapMovement)) {
g_touches_moving = true;
g_tapping = false;
} else {
g_touches_moving = false;
}
} else {
g_touches_moving = false;
}
/* Update the previous touch count. */
g_prev_touch_count = current_touch.count;
}
void ChangeMenu(std::shared_ptr<Menu> menu) {
g_current_menu = menu;
}
void ReturnToPreviousMenu() {
/* Go to the previous menu if there is one. */
if (g_current_menu->GetPrevMenu() != nullptr) {
g_current_menu = g_current_menu->GetPrevMenu();
}
}
Result IsPathBottomLevel(const char *path, bool *out) {
Result rc = 0;
FsFileSystem *fs;
char translated_path[FS_MAX_PATH] = {};
DBK_ABORT_UNLESS(fsdevTranslatePath(path, &fs, translated_path) != -1);
FsDir dir;
if (R_FAILED(rc = fsFsOpenDirectory(fs, translated_path, FsDirOpenMode_ReadDirs, &dir))) {
return rc;
}
s64 entry_count;
if (R_FAILED(rc = fsDirGetEntryCount(&dir, &entry_count))) {
return rc;
}
*out = entry_count == 0;
fsDirClose(&dir);
return rc;
}
u32 EncodeVersion(u32 major, u32 minor, u32 micro, u32 relstep = 0) {
return ((major & 0xFF) << 24) | ((minor & 0xFF) << 16) | ((micro & 0xFF) << 8) | ((relstep & 0xFF) << 8);
}
}
void Menu::AddButton(u32 id, const char *text, float x, float y, float w, float h) {
DBK_ABORT_UNLESS(id < MaxButtons);
Button button = {
.id = id,
.selected = false,
.enabled = true,
.x = x,
.y = y,
.w = w,
.h = h,
};
strncpy(button.text, text, sizeof(button.text)-1);
m_buttons[id] = button;
}
void Menu::SetButtonSelected(u32 id, bool selected) {
DBK_ABORT_UNLESS(id < MaxButtons);
auto &button = m_buttons[id];
if (button) {
button->selected = selected;
}
}
void Menu::DeselectAllButtons() {
for (auto &button : m_buttons) {
/* Ensure button is present. */
if (!button) {
continue;
}
button->selected = false;
}
}
void Menu::SetButtonEnabled(u32 id, bool enabled) {
DBK_ABORT_UNLESS(id < MaxButtons);
auto &button = m_buttons[id];
button->enabled = enabled;
}
Button *Menu::GetButton(u32 id) {
DBK_ABORT_UNLESS(id < MaxButtons);
return !m_buttons[id] ? nullptr : &(*m_buttons[id]);
}
Button *Menu::GetSelectedButton() {
for (auto &button : m_buttons) {
if (button && button->enabled && button->selected) {
return &(*button);
}
}
return nullptr;
}
Button *Menu::GetClosestButtonToSelection(Direction direction) {
const Button *selected_button = this->GetSelectedButton();
if (selected_button == nullptr || direction == Direction::Invalid) {
return nullptr;
}
Button *closest_button = nullptr;
float closest_distance = 0.0f;
for (auto &button : m_buttons) {
/* Skip absent button. */
if (!button || !button->enabled) {
continue;
}
/* Skip buttons that are in the wrong direction. */
if ((direction == Direction::Down && button->y <= selected_button->y) ||
(direction == Direction::Up && button->y >= selected_button->y) ||
(direction == Direction::Right && button->x <= selected_button->x) ||
(direction == Direction::Left && button->x >= selected_button->x)) {
continue;
}
const float x_dist = button->x - selected_button->x;
const float y_dist = button->y - selected_button->y;
const float sq_dist = x_dist * x_dist + y_dist * y_dist;
/* If we don't already have a closest button, set it. */
if (closest_button == nullptr) {
closest_button = &(*button);
closest_distance = sq_dist;
continue;
}
/* Update the closest button if this one is closer. */
if (sq_dist < closest_distance) {
closest_button = &(*button);
closest_distance = sq_dist;
}
}
return closest_button;
}
Button *Menu::GetTouchedButton() {
HidTouchScreenState current_touch;
hidGetTouchScreenStates(&current_touch, 1);
const u32 touch_count = current_touch.count;
for (u32 i = 0; i < touch_count && g_started_touching; i++) {
for (auto &button : m_buttons) {
if (button && button->enabled && button->IsPositionInBounds(current_touch.touches[i].x, current_touch.touches[i].y)) {
return &(*button);
}
}
}
return nullptr;
}
Button *Menu::GetActivatedButton() {
Button *selected_button = this->GetSelectedButton();
if (selected_button == nullptr) {
return nullptr;
}
const u64 k_down = padGetButtonsDown(&g_pad);
if (k_down & HidNpadButton_A || this->GetTouchedButton() == selected_button) {
return selected_button;
}
return nullptr;
}
void Menu::UpdateButtons() {
const u64 k_down = padGetButtonsDown(&g_pad);
Direction direction = Direction::Invalid;
if (k_down & HidNpadButton_AnyDown) {
direction = Direction::Down;
} else if (k_down & HidNpadButton_AnyUp) {
direction = Direction::Up;
} else if (k_down & HidNpadButton_AnyLeft) {
direction = Direction::Left;
} else if (k_down & HidNpadButton_AnyRight) {
direction = Direction::Right;
}
/* Select the closest button. */
if (const Button *closest_button = this->GetClosestButtonToSelection(direction); closest_button != nullptr) {
this->DeselectAllButtons();
this->SetButtonSelected(closest_button->id, true);
}
/* Select the touched button. */
if (const Button *touched_button = this->GetTouchedButton(); touched_button != nullptr) {
this->DeselectAllButtons();
this->SetButtonSelected(touched_button->id, true);
}
}
void Menu::DrawButtons(NVGcontext *vg, u64 ns) {
for (auto &button : m_buttons) {
/* Ensure button is present. */
if (!button) {
continue;
}
/* Set the button style. */
auto style = ButtonStyle::StandardDisabled;
if (button->enabled) {
style = button->selected ? ButtonStyle::StandardSelected : ButtonStyle::Standard;
}
DrawButton(vg, button->text, button->x, button->y, button->w, button->h, style, ns);
}
}
void Menu::LogText(const char *format, ...) {
/* Create a temporary string. */
char tmp[0x100];
va_list args;
va_start(args, format);
vsnprintf(tmp, sizeof(tmp), format, args);
va_end(args);
/* Append the text to the log buffer. */
strncat(m_log_buffer, tmp, sizeof(m_log_buffer)-1);
}
std::shared_ptr<Menu> Menu::GetPrevMenu() {
return m_prev_menu;
}
AlertMenu::AlertMenu(std::shared_ptr<Menu> prev_menu, const char *text, const char *subtext, Result rc) : Menu(prev_menu), m_text{}, m_subtext{}, m_result_text{}, m_rc(rc){
/* Copy the input text. */
strncpy(m_text, text, sizeof(m_text)-1);
strncpy(m_subtext, subtext, sizeof(m_subtext)-1);
/* Copy result text if there is a result. */
if (R_FAILED(rc)) {
snprintf(m_result_text, sizeof(m_result_text), "Result: 0x%08x", rc);
}
}
void AlertMenu::Draw(NVGcontext *vg, u64 ns) {
const float window_height = WindowHeight + (R_FAILED(m_rc) ? SubTextHeight : 0.0f);
const float x = g_screen_width / 2.0f - WindowWidth / 2.0f;
const float y = g_screen_height / 2.0f - window_height / 2.0f;
DrawWindow(vg, m_text, x, y, WindowWidth, window_height);
DrawText(vg, x + HorizontalInset, y + TitleGap, WindowWidth - HorizontalInset * 2.0f, m_subtext);
/* Draw the result if there is one. */
if (R_FAILED(m_rc)) {
DrawText(vg, x + HorizontalInset, y + TitleGap + SubTextHeight, WindowWidth - HorizontalInset * 2.0f, m_result_text);
}
this->DrawButtons(vg, ns);
}
ErrorMenu::ErrorMenu(const char *text, const char *subtext, Result rc) : AlertMenu(nullptr, text, subtext, rc) {
const float window_height = WindowHeight + (R_FAILED(m_rc) ? SubTextHeight : 0.0f);
const float x = g_screen_width / 2.0f - WindowWidth / 2.0f;
const float y = g_screen_height / 2.0f - window_height / 2.0f;
const float button_y = y + TitleGap + SubTextHeight + VerticalGap * 2.0f + (R_FAILED(m_rc) ? SubTextHeight : 0.0f);
const float button_width = WindowWidth - HorizontalInset * 2.0f;
/* Add buttons. */
this->AddButton(ExitButtonId, "Exit", x + HorizontalInset, button_y, button_width, ButtonHeight);
this->SetButtonSelected(ExitButtonId, true);
}
void ErrorMenu::Update(u64 ns) {
u64 k_down = padGetButtonsDown(&g_pad);
/* Go back if B is pressed. */
if (k_down & HidNpadButton_B) {
g_exit_requested = true;
return;
}
/* Take action if a button has been activated. */
if (const Button *activated_button = this->GetActivatedButton(); activated_button != nullptr) {
switch (activated_button->id) {
case ExitButtonId:
g_exit_requested = true;
break;
}
}
this->UpdateButtons();
/* Fallback on selecting the exfat button. */
if (const Button *selected_button = this->GetSelectedButton(); k_down && selected_button == nullptr) {
this->SetButtonSelected(ExitButtonId, true);
}
}
WarningMenu::WarningMenu(std::shared_ptr<Menu> prev_menu, std::shared_ptr<Menu> next_menu, const char *text, const char *subtext, Result rc) : AlertMenu(prev_menu, text, subtext, rc), m_next_menu(next_menu) {
const float window_height = WindowHeight + (R_FAILED(m_rc) ? SubTextHeight : 0.0f);
const float x = g_screen_width / 2.0f - WindowWidth / 2.0f;
const float y = g_screen_height / 2.0f - window_height / 2.0f;
const float button_y = y + TitleGap + SubTextHeight + VerticalGap * 2.0f + (R_FAILED(m_rc) ? SubTextHeight : 0.0f);
const float button_width = (WindowWidth - HorizontalInset * 2.0f) / 2.0f - ButtonHorizontalGap;
this->AddButton(BackButtonId, "Back", x + HorizontalInset, button_y, button_width, ButtonHeight);
this->AddButton(ContinueButtonId, "Continue", x + HorizontalInset + button_width + ButtonHorizontalGap, button_y, button_width, ButtonHeight);
this->SetButtonSelected(ContinueButtonId, true);
}
void WarningMenu::Update(u64 ns) {
u64 k_down = padGetButtonsDown(&g_pad);
/* Go back if B is pressed. */
if (k_down & HidNpadButton_B) {
ReturnToPreviousMenu();
return;
}
/* Take action if a button has been activated. */
if (const Button *activated_button = this->GetActivatedButton(); activated_button != nullptr) {
switch (activated_button->id) {
case BackButtonId:
ReturnToPreviousMenu();
return;
case ContinueButtonId:
ChangeMenu(m_next_menu);
return;
}
}
this->UpdateButtons();
/* Fallback on selecting the exfat button. */
if (const Button *selected_button = this->GetSelectedButton(); k_down && selected_button == nullptr) {
this->SetButtonSelected(ContinueButtonId, true);
}
}
MainMenu::MainMenu() : Menu(nullptr) {
const float x = g_screen_width / 2.0f - WindowWidth / 2.0f;
const float y = g_screen_height / 2.0f - WindowHeight / 2.0f;
this->AddButton(InstallButtonId, "Install", x + HorizontalInset, y + TitleGap, WindowWidth - HorizontalInset * 2, ButtonHeight);
this->AddButton(ExitButtonId, "Exit", x + HorizontalInset, y + TitleGap + ButtonHeight + VerticalGap, WindowWidth - HorizontalInset * 2, ButtonHeight);
this->SetButtonSelected(InstallButtonId, true);
}
void MainMenu::Update(u64 ns) {
u64 k_down = padGetButtonsDown(&g_pad);
if (k_down & HidNpadButton_B) {
g_exit_requested = true;
}
/* Take action if a button has been activated. */
if (const Button *activated_button = this->GetActivatedButton(); activated_button != nullptr) {
switch (activated_button->id) {
case InstallButtonId:
{
const auto file_menu = std::make_shared<FileMenu>(g_current_menu, "/");
Result rc = 0;
u64 hardware_type;
u64 has_rcm_bug_patch;
u64 is_emummc;
if (R_FAILED(rc = splGetConfig(SplConfigItem_HardwareType, &hardware_type))) {
ChangeMenu(std::make_shared<ErrorMenu>("An error has occurred", "Failed to get hardware type.", rc));
return;
}
if (R_FAILED(rc = splGetConfig(static_cast<SplConfigItem>(ExosphereHasRcmBugPatch), &has_rcm_bug_patch))) {
ChangeMenu(std::make_shared<ErrorMenu>("An error has occurred", "Failed to check RCM bug status.", rc));
return;
}
if (R_FAILED(rc = splGetConfig(static_cast<SplConfigItem>(ExosphereEmummcType), &is_emummc))) {
ChangeMenu(std::make_shared<ErrorMenu>("An error has occurred", "Failed to check emuMMC status.", rc));
return;
}
/* Warn if we're working with a patched unit. */
const bool is_erista = hardware_type == 0 || hardware_type == 1;
if (is_erista && has_rcm_bug_patch && !is_emummc) {
ChangeMenu(std::make_shared<WarningMenu>(g_current_menu, file_menu, "Warning: Patched unit detected", "You may burn fuses or render your switch inoperable."));
} else {
ChangeMenu(file_menu);
}
return;
}
case ExitButtonId:
g_exit_requested = true;
return;
}
}
this->UpdateButtons();
/* Fallback on selecting the install button. */
if (const Button *selected_button = this->GetSelectedButton(); k_down && selected_button == nullptr) {
this->SetButtonSelected(InstallButtonId, true);
}
}
void MainMenu::Draw(NVGcontext *vg, u64 ns) {
DrawWindow(vg, "Daybreak", g_screen_width / 2.0f - WindowWidth / 2.0f, g_screen_height / 2.0f - WindowHeight / 2.0f, WindowWidth, WindowHeight);
this->DrawButtons(vg, ns);
}
FileMenu::FileMenu(std::shared_ptr<Menu> prev_menu, const char *root) : Menu(prev_menu), m_current_index(0), m_scroll_offset(0), m_touch_start_scroll_offset(0), m_touch_finalize_selection(false) {
Result rc = 0;
strncpy(m_root, root, sizeof(m_root)-1);
if (R_FAILED(rc = this->PopulateFileEntries())) {
fatalThrow(rc);
}
}
Result FileMenu::PopulateFileEntries() {
/* Open the directory. */
DIR *dir = opendir(m_root);
if (dir == nullptr) {
return fsdevGetLastResult();
}
/* Add file entries to the list. */
struct dirent *ent;
while ((ent = readdir(dir)) != nullptr) {
if (ent->d_type == DT_DIR) {
FileEntry file_entry = {};
strncpy(file_entry.name, ent->d_name, sizeof(file_entry.name));
m_file_entries.push_back(file_entry);
}
}
/* Close the directory. */
closedir(dir);
/* Sort the file entries. */
std::sort(m_file_entries.begin(), m_file_entries.end(), [](const FileEntry &a, const FileEntry &b) {
return strncmp(a.name, b.name, sizeof(a.name)) < 0;
});
return 0;
}
bool FileMenu::IsSelectionVisible() {
const float visible_start = m_scroll_offset;
const float visible_end = visible_start + FileListHeight;
const float entry_start = static_cast<float>(m_current_index) * (FileRowHeight + FileRowGap);
const float entry_end = entry_start + (FileRowHeight + FileRowGap);
return entry_start >= visible_start && entry_end <= visible_end;
}
void FileMenu::ScrollToSelection() {
const float visible_start = m_scroll_offset;
const float visible_end = visible_start + FileListHeight;
const float entry_start = static_cast<float>(m_current_index) * (FileRowHeight + FileRowGap);
const float entry_end = entry_start + (FileRowHeight + FileRowGap);
if (entry_end > visible_end) {
m_scroll_offset += entry_end - visible_end;
} else if (entry_end < visible_end) {
m_scroll_offset = entry_start;
}
}
bool FileMenu::IsEntryTouched(u32 i) {
const float x = g_screen_width / 2.0f - WindowWidth / 2.0f;
const float y = g_screen_height / 2.0f - WindowHeight / 2.0f;
HidTouchScreenState current_touch;
hidGetTouchScreenStates(&current_touch, 1);
/* Check if the tap is within the x bounds. */
if (current_touch.touches[0].x >= x + TextBackgroundOffset + FileRowHorizontalInset && current_touch.touches[0].x <= WindowWidth - (TextBackgroundOffset + FileRowHorizontalInset) * 2.0f) {
const float y_min = y + TitleGap + FileRowGap + i * (FileRowHeight + FileRowGap) - m_scroll_offset;
const float y_max = y_min + FileRowHeight;
/* Check if the tap is within the y bounds. */
if (current_touch.touches[0].y >= y_min && current_touch.touches[0].y <= y_max) {
return true;
}
}
return false;
}
void FileMenu::UpdateTouches() {
/* Setup values on initial touch. */
if (g_started_touching) {
m_touch_start_scroll_offset = m_scroll_offset;
/* We may potentially finalize the selection later if we start off touching it. */
if (this->IsEntryTouched(m_current_index)) {
m_touch_finalize_selection = true;
}
}
/* Scroll based on touch movement. */
if (g_touches_moving) {
HidTouchScreenState current_touch;
hidGetTouchScreenStates(&current_touch, 1);
const int dist_y = current_touch.touches[0].y - g_start_touch.touches[0].y;
float new_scroll_offset = m_touch_start_scroll_offset - static_cast<float>(dist_y);
float max_scroll = (FileRowHeight + FileRowGap) * static_cast<float>(m_file_entries.size()) - FileListHeight;
/* Don't allow scrolling if there is not enough elements. */
if (max_scroll < 0.0f) {
max_scroll = 0.0f;
}
/* Don't allow scrolling before the first element. */
if (new_scroll_offset < 0.0f) {
new_scroll_offset = 0.0f;
}
/* Don't allow scrolling past the last element. */
if (new_scroll_offset > max_scroll) {
new_scroll_offset = max_scroll;
}
m_scroll_offset = new_scroll_offset;
}
/* Select any tapped entries. */
if (g_tapping) {
for (u32 i = 0; i < m_file_entries.size(); i++) {
if (this->IsEntryTouched(i)) {
/* The current index is checked later. */
if (i == m_current_index) {
continue;
}
m_current_index = i;
/* Don't finalize selection if we touch something else. */
m_touch_finalize_selection = false;
break;
}
}
}
/* Don't finalize selection if we aren't finished and we've either stopped tapping or are no longer touching the selection. */
if (!g_finished_touching && (!g_tapping || !this->IsEntryTouched(m_current_index))) {
m_touch_finalize_selection = false;
}
/* Finalize selection if the currently selected entry is touched for the second time. */
if (g_finished_touching && m_touch_finalize_selection) {
this->FinalizeSelection();
m_touch_finalize_selection = false;
}
}
void FileMenu::FinalizeSelection() {
DBK_ABORT_UNLESS(m_current_index < m_file_entries.size());
FileEntry &entry = m_file_entries[m_current_index];
/* Determine the selected path. */
char current_path[FS_MAX_PATH] = {};
const int path_len = snprintf(current_path, sizeof(current_path), "%s%s/", m_root, entry.name);
DBK_ABORT_UNLESS(path_len >= 0 && path_len < static_cast<int>(sizeof(current_path)));
/* Determine if the chosen path is the bottom level. */
Result rc = 0;
bool bottom_level;
if (R_FAILED(rc = IsPathBottomLevel(current_path, &bottom_level))) {
fatalThrow(rc);
}
/* Show exfat settings or the next file menu. */
if (bottom_level) {
/* Set the update path. */
snprintf(g_update_path, sizeof(g_update_path), "%s", current_path);
/* Change the menu. */
ChangeMenu(std::make_shared<ValidateUpdateMenu>(g_current_menu));
} else {
ChangeMenu(std::make_shared<FileMenu>(g_current_menu, current_path));
}
}
void FileMenu::Update(u64 ns) {
u64 k_down = padGetButtonsDown(&g_pad);
/* Go back if B is pressed. */
if (k_down & HidNpadButton_B) {
ReturnToPreviousMenu();
return;
}
/* Finalize selection on pressing A. */
if (k_down & HidNpadButton_A) {
this->FinalizeSelection();
}
/* Update touch input. */
this->UpdateTouches();
const u32 prev_index = m_current_index;
if (k_down & HidNpadButton_AnyDown) {
/* Scroll down. */
if (m_current_index >= (m_file_entries.size() - 1)) {
m_current_index = 0;
} else {
m_current_index++;
}
} else if (k_down & HidNpadButton_AnyUp) {
/* Scroll up. */
if (m_current_index == 0) {
m_current_index = m_file_entries.size() - 1;
} else {
m_current_index--;
}
}
/* Scroll to the selection if it isn't visible. */
if (prev_index != m_current_index && !this->IsSelectionVisible()) {
this->ScrollToSelection();
}
}
void FileMenu::Draw(NVGcontext *vg, u64 ns) {
const float x = g_screen_width / 2.0f - WindowWidth / 2.0f;
const float y = g_screen_height / 2.0f - WindowHeight / 2.0f;
DrawWindow(vg, "Select an update directory", x, y, WindowWidth, WindowHeight);
DrawTextBackground(vg, x + TextBackgroundOffset, y + TitleGap, WindowWidth - TextBackgroundOffset * 2.0f, (FileRowHeight + FileRowGap) * MaxFileRows + FileRowGap);
nvgSave(vg);
nvgScissor(vg, x + TextBackgroundOffset, y + TitleGap, WindowWidth - TextBackgroundOffset * 2.0f, (FileRowHeight + FileRowGap) * MaxFileRows + FileRowGap);
for (u32 i = 0; i < m_file_entries.size(); i++) {
FileEntry &entry = m_file_entries[i];
auto style = ButtonStyle::FileSelect;
if (i == m_current_index) {
style = ButtonStyle::FileSelectSelected;
}
DrawButton(vg, entry.name, x + TextBackgroundOffset + FileRowHorizontalInset, y + TitleGap + FileRowGap + i * (FileRowHeight + FileRowGap) - m_scroll_offset, WindowWidth - (TextBackgroundOffset + FileRowHorizontalInset) * 2.0f, FileRowHeight, style, ns);
}
nvgRestore(vg);
}
ValidateUpdateMenu::ValidateUpdateMenu(std::shared_ptr<Menu> prev_menu) : Menu(prev_menu), m_has_drawn(false), m_has_info(false), m_has_validated(false) {
const float x = g_screen_width / 2.0f - WindowWidth / 2.0f;
const float y = g_screen_height / 2.0f - WindowHeight / 2.0f;
const float button_width = (WindowWidth - HorizontalInset * 2.0f) / 2.0f - ButtonHorizontalGap;
/* Add buttons. */
this->AddButton(BackButtonId, "Back", x + HorizontalInset, y + WindowHeight - BottomInset - ButtonHeight, button_width, ButtonHeight);
this->AddButton(ContinueButtonId, "Continue", x + HorizontalInset + button_width + ButtonHorizontalGap, y + WindowHeight - BottomInset - ButtonHeight, button_width, ButtonHeight);
this->SetButtonEnabled(BackButtonId, false);
this->SetButtonEnabled(ContinueButtonId, false);
/* Obtain update information. */
if (R_FAILED(this->GetUpdateInformation())) {
this->SetButtonEnabled(BackButtonId, true);
this->SetButtonSelected(BackButtonId, true);
} else {
/* Log this early so it is printed out before validation causes stalling. */
this->LogText("Validating update, this may take a moment...\n");
}
}
Result ValidateUpdateMenu::GetUpdateInformation() {
Result rc = 0;
this->LogText("Directory %s\n", g_update_path);
/* Attempt to get the update information. */
if (R_FAILED(rc = amssuGetUpdateInformation(&m_update_info, g_update_path))) {
if (rc == 0x1a405) {
this->LogText("No update found in folder.\nEnsure your ncas are named correctly!\nResult: 0x%08x\n", rc);
} else {
this->LogText("Failed to get update information.\nResult: 0x%08x\n", rc);
}
return rc;
}
/* Print update information. */
this->LogText("- Version: %d.%d.%d\n", (m_update_info.version >> 26) & 0x1f, (m_update_info.version >> 20) & 0x1f, (m_update_info.version >> 16) & 0xf);
if (m_update_info.exfat_supported) {
this->LogText("- exFAT: Supported\n");
} else {
this->LogText("- exFAT: Unsupported\n");
}
this->LogText("- Firmware variations: %d\n", m_update_info.num_firmware_variations);
/* Mark as having obtained update info. */
m_has_info = true;
return rc;
}
void ValidateUpdateMenu::ValidateUpdate() {
Result rc = 0;
/* Validate the update. */
if (R_FAILED(rc = amssuValidateUpdate(&m_validation_info, g_update_path))) {
this->LogText("Failed to validate update.\nResult: 0x%08x\n", rc);
return;
}
/* Check the result. */
if (R_SUCCEEDED(m_validation_info.result)) {
this->LogText("Update is valid!\n");
if (R_FAILED(m_validation_info.exfat_result)) {
const u32 version = m_validation_info.invalid_key.version;
this->LogText("exFAT Validation failed with result: 0x%08x\n", m_validation_info.exfat_result);
this->LogText("Missing content:\n- Program id: %016lx\n- Version: %d.%d.%d\n", m_validation_info.invalid_key.id, (version >> 26) & 0x1f, (version >> 20) & 0x1f, (version >> 16) & 0xf);
/* Log the missing content id. */
this->LogText("- Content id: ");
for (size_t i = 0; i < sizeof(NcmContentId); i++) {
this->LogText("%02x", m_validation_info.invalid_content_id.c[i]);
}
this->LogText("\n");
}
/* Enable the back and continue buttons and select the continue button. */
this->SetButtonEnabled(BackButtonId, true);
this->SetButtonEnabled(ContinueButtonId, true);
this->SetButtonSelected(ContinueButtonId, true);
} else {
/* Log the missing content info. */
const u32 version = m_validation_info.invalid_key.version;
this->LogText("Validation failed with result: 0x%08x\n", m_validation_info.result);
this->LogText("Missing content:\n- Program id: %016lx\n- Version: %d.%d.%d\n", m_validation_info.invalid_key.id, (version >> 26) & 0x1f, (version >> 20) & 0x1f, (version >> 16) & 0xf);
/* Log the missing content id. */
this->LogText("- Content id: ");
for (size_t i = 0; i < sizeof(NcmContentId); i++) {
this->LogText("%02x", m_validation_info.invalid_content_id.c[i]);
}
this->LogText("\n");
/* Enable the back button and select it. */
this->SetButtonEnabled(BackButtonId, true);
this->SetButtonSelected(BackButtonId, true);
}
/* Mark validation as being complete. */
m_has_validated = true;
}
void ValidateUpdateMenu::Update(u64 ns) {
/* Perform validation if it hasn't been done already. */
if (m_has_info && m_has_drawn && !m_has_validated) {
this->ValidateUpdate();
}
u64 k_down = padGetButtonsDown(&g_pad);
/* Go back if B is pressed. */
if (k_down & HidNpadButton_B) {
ReturnToPreviousMenu();
return;
}
/* Take action if a button has been activated. */
if (const Button *activated_button = this->GetActivatedButton(); activated_button != nullptr) {
switch (activated_button->id) {
case BackButtonId:
ReturnToPreviousMenu();
return;
case ContinueButtonId:
/* Don't continue if validation hasn't been done or has failed. */
if (!m_has_validated || R_FAILED(m_validation_info.result)) {
break;
}
/* Check if exfat is supported. */
g_exfat_supported = m_update_info.exfat_supported && R_SUCCEEDED(m_validation_info.exfat_result);
if (!g_exfat_supported) {
g_use_exfat = false;
}
/* Create the next menu. */
std::shared_ptr<Menu> next_menu = std::make_shared<ChooseResetMenu>(g_current_menu);
/* Warn the user if they're updating with exFAT supposed to be supported but not present/corrupted. */
if (m_update_info.exfat_supported && R_FAILED(m_validation_info.exfat_result)) {
next_menu = std::make_shared<WarningMenu>(g_current_menu, next_menu, "Warning: exFAT firmware is missing or corrupt", "Are you sure you want to proceed?");
}
/* Warn the user if they're updating to a version higher than supported. */
const u32 version = m_validation_info.invalid_key.version;
if (EncodeVersion((version >> 26) & 0x1f, (version >> 20) & 0x1f, (version >> 16) & 0xf) > g_supported_version) {
next_menu = std::make_shared<WarningMenu>(g_current_menu, next_menu, "Warning: firmware is too new and not known to be supported", "Are you sure you want to proceed?");
}
/* Change to the next menu. */
ChangeMenu(next_menu);
return;
}
}
this->UpdateButtons();
}
void ValidateUpdateMenu::Draw(NVGcontext *vg, u64 ns) {
const float x = g_screen_width / 2.0f - WindowWidth / 2.0f;
const float y = g_screen_height / 2.0f - WindowHeight / 2.0f;
DrawWindow(vg, "Update information", x, y, WindowWidth, WindowHeight);
DrawTextBackground(vg, x + HorizontalInset, y + TitleGap, WindowWidth - HorizontalInset * 2.0f, TextAreaHeight);
DrawTextBlock(vg, m_log_buffer, x + HorizontalInset + TextHorizontalInset, y + TitleGap + TextVerticalInset, WindowWidth - (HorizontalInset + TextHorizontalInset) * 2.0f, TextAreaHeight - TextVerticalInset * 2.0f);
this->DrawButtons(vg, ns);
m_has_drawn = true;
}
ChooseResetMenu::ChooseResetMenu(std::shared_ptr<Menu> prev_menu) : Menu(prev_menu) {
const float x = g_screen_width / 2.0f - WindowWidth / 2.0f;
const float y = g_screen_height / 2.0f - WindowHeight / 2.0f;
const float button_width = (WindowWidth - HorizontalInset * 2.0f) / 2.0f - ButtonHorizontalGap;
/* Add buttons. */
this->AddButton(ResetToFactorySettingsButtonId, "Reset to factory settings", x + HorizontalInset, y + TitleGap, button_width, ButtonHeight);
this->AddButton(PreserveSettingsButtonId, "Preserve settings", x + HorizontalInset + button_width + ButtonHorizontalGap, y + TitleGap, button_width, ButtonHeight);
this->SetButtonSelected(PreserveSettingsButtonId, true);
}
void ChooseResetMenu::Update(u64 ns) {
u64 k_down = padGetButtonsDown(&g_pad);
/* Go back if B is pressed. */
if (k_down & HidNpadButton_B) {
ReturnToPreviousMenu();
return;
}
/* Take action if a button has been activated. */
if (const Button *activated_button = this->GetActivatedButton(); activated_button != nullptr) {
switch (activated_button->id) {
case ResetToFactorySettingsButtonId:
g_reset_to_factory = true;
break;
case PreserveSettingsButtonId:
g_reset_to_factory = false;
break;
}
std::shared_ptr<Menu> next_menu;
if (g_exfat_supported) {
next_menu = std::make_shared<ChooseExfatMenu>(g_current_menu);
} else {
next_menu = std::make_shared<WarningMenu>(g_current_menu, std::make_shared<InstallUpdateMenu>(g_current_menu), "Ready to begin update installation", "Are you sure you want to proceed?");
}
if (g_reset_to_factory) {
ChangeMenu(std::make_shared<WarningMenu>(g_current_menu, next_menu, "Warning: Factory reset selected", "Saves and installed games will be permanently deleted."));
} else {
ChangeMenu(next_menu);
}
}
this->UpdateButtons();
/* Fallback on selecting the exfat button. */
if (const Button *selected_button = this->GetSelectedButton(); k_down && selected_button == nullptr) {
this->SetButtonSelected(PreserveSettingsButtonId, true);
}
}
void ChooseResetMenu::Draw(NVGcontext *vg, u64 ns) {
const float x = g_screen_width / 2.0f - WindowWidth / 2.0f;
const float y = g_screen_height / 2.0f - WindowHeight / 2.0f;
DrawWindow(vg, "Select settings mode", x, y, WindowWidth, WindowHeight);
this->DrawButtons(vg, ns);
}
ChooseExfatMenu::ChooseExfatMenu(std::shared_ptr<Menu> prev_menu) : Menu(prev_menu) {
const float x = g_screen_width / 2.0f - WindowWidth / 2.0f;
const float y = g_screen_height / 2.0f - WindowHeight / 2.0f;
const float button_width = (WindowWidth - HorizontalInset * 2.0f) / 2.0f - ButtonHorizontalGap;
/* Add buttons. */
this->AddButton(Fat32ButtonId, "Install (FAT32)", x + HorizontalInset, y + TitleGap, button_width, ButtonHeight);
this->AddButton(ExFatButtonId, "Install (FAT32 + exFAT)", x + HorizontalInset + button_width + ButtonHorizontalGap, y + TitleGap, button_width, ButtonHeight);
/* Set the default selected button based on the user's current install. We aren't particularly concerned if fsIsExFatSupported fails. */
bool exfat_supported = false;
fsIsExFatSupported(&exfat_supported);
if (exfat_supported) {
this->SetButtonSelected(ExFatButtonId, true);
} else {
this->SetButtonSelected(Fat32ButtonId, true);
}
}
void ChooseExfatMenu::Update(u64 ns) {
u64 k_down = padGetButtonsDown(&g_pad);
/* Go back if B is pressed. */
if (k_down & HidNpadButton_B) {
ReturnToPreviousMenu();
return;
}
/* Take action if a button has been activated. */
if (const Button *activated_button = this->GetActivatedButton(); activated_button != nullptr) {
switch (activated_button->id) {
case Fat32ButtonId:
g_use_exfat = false;
break;
case ExFatButtonId:
g_use_exfat = true;
break;
}
ChangeMenu(std::make_shared<WarningMenu>(g_current_menu, std::make_shared<InstallUpdateMenu>(g_current_menu), "Ready to begin update installation", "Are you sure you want to proceed?"));
}
this->UpdateButtons();
/* Fallback on selecting the exfat button. */
if (const Button *selected_button = this->GetSelectedButton(); k_down && selected_button == nullptr) {
this->SetButtonSelected(ExFatButtonId, true);
}
}
void ChooseExfatMenu::Draw(NVGcontext *vg, u64 ns) {
const float x = g_screen_width / 2.0f - WindowWidth / 2.0f;
const float y = g_screen_height / 2.0f - WindowHeight / 2.0f;
DrawWindow(vg, "Select driver variant", x, y, WindowWidth, WindowHeight);
this->DrawButtons(vg, ns);
}
InstallUpdateMenu::InstallUpdateMenu(std::shared_ptr<Menu> prev_menu) : Menu(prev_menu), m_install_state(InstallState::NeedsDraw), m_progress_percent(0.0f) {
const float x = g_screen_width / 2.0f - WindowWidth / 2.0f;
const float y = g_screen_height / 2.0f - WindowHeight / 2.0f;
const float button_width = (WindowWidth - HorizontalInset * 2.0f) / 2.0f - ButtonHorizontalGap;
/* Add buttons. */
this->AddButton(ShutdownButtonId, "Shutdown", x + HorizontalInset, y + WindowHeight - BottomInset - ButtonHeight, button_width, ButtonHeight);
this->AddButton(RebootButtonId, "Reboot", x + HorizontalInset + button_width + ButtonHorizontalGap, y + WindowHeight - BottomInset - ButtonHeight, button_width, ButtonHeight);
this->SetButtonEnabled(ShutdownButtonId, false);
this->SetButtonEnabled(RebootButtonId, false);
/* Prevent the home button from being pressed during installation. */
hiddbgDeactivateHomeButton();
}
void InstallUpdateMenu::MarkForReboot() {
this->SetButtonEnabled(ShutdownButtonId, true);
this->SetButtonEnabled(RebootButtonId, true);
this->SetButtonSelected(RebootButtonId, true);
m_install_state = InstallState::AwaitingReboot;
}
Result InstallUpdateMenu::TransitionUpdateState() {
Result rc = 0;
if (m_install_state == InstallState::NeedsSetup) {
/* Setup the update. */
if (R_FAILED(rc = amssuSetupUpdate(nullptr, UpdateTaskBufferSize, g_update_path, g_use_exfat))) {
this->LogText("Failed to setup update.\nResult: 0x%08x\n", rc);
this->MarkForReboot();
return rc;
}
/* Log setup completion. */
this->LogText("Update setup complete.\n");
m_install_state = InstallState::NeedsPrepare;
} else if (m_install_state == InstallState::NeedsPrepare) {
/* Request update preparation. */
if (R_FAILED(rc = amssuRequestPrepareUpdate(&m_prepare_result))) {
this->LogText("Failed to request update preparation.\nResult: 0x%08x\n", rc);
this->MarkForReboot();
return rc;
}
/* Log awaiting prepare. */
this->LogText("Preparing update...\n");
m_install_state = InstallState::AwaitingPrepare;
} else if (m_install_state == InstallState::AwaitingPrepare) {
/* Check if preparation has a result. */
if (R_FAILED(rc = asyncResultWait(&m_prepare_result, 0)) && rc != 0xea01) {
this->LogText("Failed to check update preparation result.\nResult: 0x%08x\n", rc);
this->MarkForReboot();
return rc;
} else if (R_SUCCEEDED(rc)) {
if (R_FAILED(rc = asyncResultGet(&m_prepare_result))) {
this->LogText("Failed to prepare update.\nResult: 0x%08x\n", rc);
this->MarkForReboot();
return rc;
}
}
/* Check if the update has been prepared. */
bool prepared;
if (R_FAILED(rc = amssuHasPreparedUpdate(&prepared))) {
this->LogText("Failed to check if update has been prepared.\nResult: 0x%08x\n", rc);
this->MarkForReboot();
return rc;
}
/* Mark for application if preparation complete. */
if (prepared) {
this->LogText("Update preparation complete.\nApplying update...\n");
m_install_state = InstallState::NeedsApply;
return rc;
}
/* Check update progress. */
NsSystemUpdateProgress update_progress = {};
if (R_FAILED(rc = amssuGetPrepareUpdateProgress(&update_progress))) {
this->LogText("Failed to check update progress.\nResult: 0x%08x\n", rc);
this->MarkForReboot();
return rc;
}
/* Update progress percent. */
if (update_progress.total_size > 0.0f) {
m_progress_percent = static_cast<float>(update_progress.current_size) / static_cast<float>(update_progress.total_size);
} else {
m_progress_percent = 0.0f;
}
} else if (m_install_state == InstallState::NeedsApply) {
/* Apply the prepared update. */
if (R_FAILED(rc = amssuApplyPreparedUpdate())) {
this->LogText("Failed to apply update.\nResult: 0x%08x\n", rc);
} else {
/* Log success. */
this->LogText("Update applied successfully.\n");
if (g_reset_to_factory) {
if (R_FAILED(rc = nsResetToFactorySettingsForRefurbishment())) {
/* Fallback on ResetToFactorySettings. */
if (rc == MAKERESULT(Module_Libnx, LibnxError_IncompatSysVer)) {
if (R_FAILED(rc = nsResetToFactorySettings())) {
this->LogText("Failed to reset to factory settings.\nResult: 0x%08x\n", rc);
this->MarkForReboot();
return rc;
}
} else {
this->LogText("Failed to reset to factory settings for refurbishment.\nResult: 0x%08x\n", rc);
this->MarkForReboot();
return rc;
}
}
this->LogText("Successfully reset to factory settings.\n", rc);
}
}
this->MarkForReboot();
return rc;
}
return rc;
}
void InstallUpdateMenu::Update(u64 ns) {
/* Transition to the next update state. */
if (m_install_state != InstallState::NeedsDraw && m_install_state != InstallState::AwaitingReboot) {
this->TransitionUpdateState();
}
/* Take action if a button has been activated. */
if (const Button *activated_button = this->GetActivatedButton(); activated_button != nullptr) {
switch (activated_button->id) {
case ShutdownButtonId:
if (R_FAILED(appletRequestToShutdown())) {
spsmShutdown(false);
}
break;
case RebootButtonId:
if (R_FAILED(appletRequestToReboot())) {
spsmShutdown(true);
}
break;
}
}
this->UpdateButtons();
}
void InstallUpdateMenu::Draw(NVGcontext *vg, u64 ns) {
const float x = g_screen_width / 2.0f - WindowWidth / 2.0f;
const float y = g_screen_height / 2.0f - WindowHeight / 2.0f;
DrawWindow(vg, "Installing update", x, y, WindowWidth, WindowHeight);
DrawProgressText(vg, x + HorizontalInset, y + TitleGap, m_progress_percent);
DrawProgressBar(vg, x + HorizontalInset, y + TitleGap + ProgressTextHeight, WindowWidth - HorizontalInset * 2.0f, ProgressBarHeight, m_progress_percent);
DrawTextBackground(vg, x + HorizontalInset, y + TitleGap + ProgressTextHeight + ProgressBarHeight + VerticalGap, WindowWidth - HorizontalInset * 2.0f, TextAreaHeight);
DrawTextBlock(vg, m_log_buffer, x + HorizontalInset + TextHorizontalInset, y + TitleGap + ProgressTextHeight + ProgressBarHeight + VerticalGap + TextVerticalInset, WindowWidth - (HorizontalInset + TextHorizontalInset) * 2.0f, TextAreaHeight - TextVerticalInset * 2.0f);
this->DrawButtons(vg, ns);
/* We have drawn now, allow setup to occur. */
if (m_install_state == InstallState::NeedsDraw) {
this->LogText("Beginning update setup...\n");
m_install_state = InstallState::NeedsSetup;
}
}
bool InitializeMenu(u32 screen_width, u32 screen_height) {
Result rc = 0;
/* Configure and initialize the gamepad. */
padConfigureInput(1, HidNpadStyleSet_NpadStandard);
padInitializeDefault(&g_pad);
/* Initialize the touch screen. */
hidInitializeTouchScreen();
/* Set the screen width and height. */
g_screen_width = screen_width;
g_screen_height = screen_height;
/* Mark as initialized. */
g_initialized = true;
/* Attempt to get the exosphere version. */
u64 version;
if (R_FAILED(rc = splGetConfig(static_cast<SplConfigItem>(ExosphereApiVersionConfigItem), &version))) {
ChangeMenu(std::make_shared<ErrorMenu>("Atmosphere not found", "Daybreak requires Atmosphere to be installed.", rc));
return false;
}
const u32 version_micro = (version >> 40) & 0xff;
const u32 version_minor = (version >> 48) & 0xff;
const u32 version_major = (version >> 56) & 0xff;
/* Validate the exosphere version. */
const bool ams_supports_sysupdate_api = EncodeVersion(version_major, version_minor, version_micro) >= EncodeVersion(0, 14, 0);
if (!ams_supports_sysupdate_api) {
ChangeMenu(std::make_shared<ErrorMenu>("Outdated Atmosphere version", "Daybreak requires Atmosphere 0.14.0 or later.", rc));
return false;
}
/* Ensure DayBreak is ran as a NRO. */
if (envIsNso()) {
ChangeMenu(std::make_shared<ErrorMenu>("Unsupported Environment", "Please launch Daybreak via the Homebrew menu.", rc));
return false;
}
/* Attempt to get the supported version. */
if (R_SUCCEEDED(rc = splGetConfig(static_cast<SplConfigItem>(ExosphereSupportedHosVersion), &version))) {
g_supported_version = static_cast<u32>(version);
}
/* Initialize ams:su. */
if (R_FAILED(rc = amssuInitialize())) {
fatalThrow(rc);
}
/* Change the current menu to the main menu. */
g_current_menu = std::make_shared<MainMenu>();
return true;
}
bool InitializeMenu(u32 screen_width, u32 screen_height, const char *update_path) {
if (InitializeMenu(screen_width, screen_height)) {
/* Set the update path. */
strncpy(g_update_path, update_path, sizeof(g_update_path));
/* Change the menu. */
ChangeMenu(std::make_shared<ValidateUpdateMenu>(g_current_menu));
return true;
}
return false;
}
void UpdateMenu(u64 ns) {
DBK_ABORT_UNLESS(g_initialized);
DBK_ABORT_UNLESS(g_current_menu != nullptr);
UpdateInput();
g_current_menu->Update(ns);
}
void RenderMenu(NVGcontext *vg, u64 ns) {
DBK_ABORT_UNLESS(g_initialized);
DBK_ABORT_UNLESS(g_current_menu != nullptr);
/* Draw background. */
DrawBackground(vg, g_screen_width, g_screen_height);
/* Draw stars. */
DrawStar(vg, 40.0f, 64.0f, 3.0f);
DrawStar(vg, 110.0f, 300.0f, 3.0f);
DrawStar(vg, 200.0f, 150.0f, 4.0f);
DrawStar(vg, 370.0f, 280.0f, 3.0f);
DrawStar(vg, 450.0f, 40.0f, 3.5f);
DrawStar(vg, 710.0f, 90.0f, 3.0f);
DrawStar(vg, 900.0f, 240.0f, 3.0f);
DrawStar(vg, 970.0f, 64.0f, 4.0f);
DrawStar(vg, 1160.0f, 160.0f, 3.5f);
DrawStar(vg, 1210.0f, 350.0f, 3.0f);
g_current_menu->Draw(vg, ns);
}
bool IsExitRequested() {
return g_exit_requested;
}
}