Files
sphaira/sphaira/source/app.cpp
ITotalJustice 5e315bd65f many fixes and performance improvements for network requests (see commit details)
- add etag support
- add last-modified support

with the above 2 changes, this means that all downloads can be cached. when attempting to download a file,
if the file is an image, load from cache. after, the download is processed with the above tags sent. if a 304 code
is received, then the file hasn't changed. otherwise, the new tags are saved and the downloaded file is now used (in the
case of an image, the new image is now loaded over the cached one).

this results in a *huge* speed improvement and overall a huge amount of bandwidth is saved for both the client and server.

- themezer requests now only request the data needed.

this results in a json file that is 4-5x smaller, meaning a much faster download and parsing time.

- loading images is capped to 2 images a frame. this was done to avoid fs being the bottle neck.
  a 9 page listing will take 5 frames. scrolling through lists is more responsive.

- downloads are pushed to the front of the queue as they're added. the point of this is to prioritise
  data that we need now.

- fix potential crash when sorting files based on names as its possible for a file to have the same name
  in the metadata. this fallsback to sorting by path, which is unique.

- add timeout for processing events. this was done in order to not block the main thread for too long.

- github json files have changed from a name + url to a repo + author pair.
- drawing widgets now starts from the last file in the array. as a menu takes up the whole screen, it
 is pointless drawing menu's underneath. this halves gpu usage.
- download url caching has been removed. this was added to fix a race condition when opening /
  closing a widget which starts a download when created. this would result in 2 same files being
  downloaded at the same time. this is no longer an issue and was overhead per download request.
2024-12-29 00:33:31 +00:00

1322 lines
42 KiB
C++

#include "ui/menus/main_menu.hpp"
#include "ui/error_box.hpp"
#include "app.hpp"
#include "log.hpp"
#include "ui/nvg_util.hpp"
#include "nro.hpp"
#include "evman.hpp"
#include "owo.hpp"
#include "image.hpp"
#include "nxlink.h"
#include "fs.hpp"
#include "defines.hpp"
#include "i18n.hpp"
#include "ftpsrv_helper.hpp"
#include <nanovg_dk.h>
#include <minIni.h>
#include <pulsar.h>
#include <haze.h>
#include <algorithm>
#include <cassert>
#include <cstring>
#include <ctime>
#include <span>
#include <dirent.h>
extern "C" {
u32 __nx_applet_exit_mode = 0;
} // extern "C"
namespace sphaira {
namespace {
constinit App* g_app{};
void deko3d_error_cb(void* userData, const char* context, DkResult result, const char* message) {
switch (result) {
case DkResult_Success:
break;
case DkResult_Fail:
log_write("[DkResult_Fail] %s\n", message);
App::Notify("DkResult_Fail");
break;
case DkResult_Timeout:
log_write("[DkResult_Timeout] %s\n", message);
App::Notify("DkResult_Timeout");
break;
case DkResult_OutOfMemory:
log_write("[DkResult_OutOfMemory] %s\n", message);
App::Notify("DkResult_OutOfMemory");
break;
case DkResult_NotImplemented:
log_write("[DkResult_NotImplemented] %s\n", message);
App::Notify("DkResult_NotImplemented");
break;
case DkResult_MisalignedSize:
log_write("[DkResult_MisalignedSize] %s\n", message);
App::Notify("DkResult_MisalignedSize");
break;
case DkResult_MisalignedData:
log_write("[DkResult_MisalignedData] %s\n", message);
App::Notify("DkResult_MisalignedData");
break;
case DkResult_BadInput:
log_write("[DkResult_BadInput] %s\n", message);
App::Notify("DkResult_BadInput");
break;
case DkResult_BadFlags:
log_write("[DkResult_BadFlags] %s\n", message);
App::Notify("DkResult_BadFlags");
break;
case DkResult_BadState:
log_write("[DkResult_BadState] %s\n", message);
App::Notify("DkResult_BadState");
break;
}
}
void on_applet_focus_state(App* app) {
switch (appletGetFocusState()) {
case AppletFocusState_InFocus:
log_write("[APPLET] AppletFocusState_InFocus\n");
// App::Notify("AppletFocusState_InFocus");
break;
case AppletFocusState_OutOfFocus:
log_write("[APPLET] AppletFocusState_OutOfFocus\n");
// App::Notify("AppletFocusState_OutOfFocus");
break;
case AppletFocusState_Background:
log_write("[APPLET] AppletFocusState_Background\n");
// App::Notify("AppletFocusState_Background");
break;
}
}
void on_applet_operation_mode(App* app) {
switch (appletGetOperationMode()) {
case AppletOperationMode_Handheld:
log_write("[APPLET] AppletOperationMode_Handheld\n");
App::Notify("Switch-Handheld!"_i18n);
break;
case AppletOperationMode_Console:
log_write("[APPLET] AppletOperationMode_Console\n");
App::Notify("Switch-Docked!"_i18n);
break;
}
}
void applet_on_performance_mode(App* app) {
switch (appletGetPerformanceMode()) {
case ApmPerformanceMode_Invalid:
log_write("[APPLET] ApmPerformanceMode_Invalid\n");
App::Notify("ApmPerformanceMode_Invalid");
break;
case ApmPerformanceMode_Normal:
log_write("[APPLET] ApmPerformanceMode_Normal\n");
App::Notify("ApmPerformanceMode_Normal");
break;
case ApmPerformanceMode_Boost:
log_write("[APPLET] ApmPerformanceMode_Boost\n");
App::Notify("ApmPerformanceMode_Boost");
break;
}
}
void appplet_hook_calback(AppletHookType type, void *param) {
auto app = static_cast<App*>(param);
switch (type) {
case AppletHookType_OnFocusState:
// App::Notify("AppletHookType_OnFocusState");
on_applet_focus_state(app);
break;
case AppletHookType_OnOperationMode:
// App::Notify("AppletHookType_OnOperationMode");
on_applet_operation_mode(app);
break;
case AppletHookType_OnPerformanceMode:
// App::Notify("AppletHookType_OnPerformanceMode");
applet_on_performance_mode(app);
break;
case AppletHookType_OnExitRequest:
// App::Notify("AppletHookType_OnExitRequest");
break;
case AppletHookType_OnResume:
// App::Notify("AppletHookType_OnResume");
break;
case AppletHookType_OnCaptureButtonShortPressed:
// App::Notify("AppletHookType_OnCaptureButtonShortPressed");
break;
case AppletHookType_OnAlbumScreenShotTaken:
// App::Notify("AppletHookType_OnAlbumScreenShotTaken");
break;
case AppletHookType_RequestToDisplay:
// App::Notify("AppletHookType_RequestToDisplay");
break;
case AppletHookType_Max:
assert(!"AppletHookType_Max hit");
break;
}
}
// this will try to decompress the icon and then re-convert it to jpg
// in order to strip exif data.
// this doesn't take long at all, but it's very overkill.
// todo: look into jpeg/exif spec to manually strip data
auto GetNroIcon(const std::vector<u8>& nro_icon) -> std::vector<u8> {
auto image = ImageLoadFromMemory(nro_icon);
if (!image.data.empty()) {
if (image.w != 256 || image.h != 256) {
image = ImageResize(image.data, image.w, image.h, 256, 256);
}
if (!image.data.empty()) {
image = ImageConvertToJpg(image.data, image.w, image.h);
if (!image.data.empty()) {
return image.data;
}
}
}
return nro_icon;
}
void haze_callback(const HazeCallbackData *data) {
App::NotifyFlashLed();
evman::push(*data, false);
}
void nxlink_callback(const NxlinkCallbackData *data) {
App::NotifyFlashLed();
evman::push(*data, false);
}
void on_i18n_change() {
i18n::exit();
i18n::init(App::GetLanguage());
}
} // namespace
void App::Loop() {
while (!m_quit && appletMainLoop()) {
if (m_widgets.empty()) {
m_quit = true;
break;
}
ui::gfx::updateHighlightAnimation();
// fire all events in in a 3ms timeslice
TimeStamp ts_event;
const u64 event_timeout = 3;
// limit events to a max per frame in order to not block for too long.
while (true) {
if (ts_event.GetMs() >= event_timeout) {
log_write("event loop timed-out\n");
break;
}
auto event = evman::pop();
if (!event.has_value()) {
break;
}
std::visit([this](auto&& arg){
using T = std::decay_t<decltype(arg)>;
if constexpr(std::is_same_v<T, evman::LaunchNroEventData>) {
log_write("[LaunchNroEventData] got event\n");
u64 timestamp = 0;
timeGetCurrentTime(TimeType_LocalSystemClock, &timestamp);
const auto nro_path = nro_normalise_path(arg.path);
ini_puts("paths", "last_launch_full", arg.argv.c_str(), App::CONFIG_PATH);
ini_puts("paths", "last_launch_path", nro_path.c_str(), App::CONFIG_PATH);
// update timestamp
ini_putl(nro_path.c_str(), "timestamp", timestamp, App::PLAYLOG_PATH);
// update launch_count
const long old_launch_count = ini_getl(nro_path.c_str(), "launch_count", 0, App::PLAYLOG_PATH);
ini_putl(nro_path.c_str(), "launch_count", old_launch_count + 1, App::PLAYLOG_PATH);
log_write("updating timestamp and launch count for: %s %lu %ld\n", nro_path.c_str(), timestamp, old_launch_count + 1);
// force disable pop-back to main menu.
__nx_applet_exit_mode = 0;
m_quit = true;
} else if constexpr(std::is_same_v<T, evman::ExitEventData>) {
log_write("[ExitEventData] got event\n");
m_quit = true;
} else if constexpr(std::is_same_v<T, HazeCallbackData>) {
// log_write("[ExitEventData] got event\n");
// m_quit = true;
} else if constexpr(std::is_same_v<T, NxlinkCallbackData>) {
switch (arg.type) {
case NxlinkCallbackType_Connected:
log_write("[NxlinkCallbackType_Connected]\n");
App::Notify("Nxlink Connected"_i18n);
break;
case NxlinkCallbackType_WriteBegin:
log_write("[NxlinkCallbackType_WriteBegin] %s\n", arg.file.filename);
App::Notify("Nxlink Upload"_i18n);
break;
case NxlinkCallbackType_WriteProgress:
// log_write("[NxlinkCallbackType_WriteProgress]\n");
break;
case NxlinkCallbackType_WriteEnd:
log_write("[NxlinkCallbackType_WriteEnd] %s\n", arg.file.filename);
App::Notify("Nxlink Finished"_i18n);
break;
}
} else if constexpr(std::is_same_v<T, curl::DownloadEventData>) {
log_write("[DownloadEventData] got event\n");
arg.callback(arg.result);
} else {
static_assert(false, "non-exhaustive visitor!");
}
}, event.value());
}
u32 w{},h{};
switch (appletGetOperationMode()) {
case AppletOperationMode_Handheld:
w = 1280;
h = 720;
break;
case AppletOperationMode_Console:
w = 1920;
h = 1080;
break;
}
if (w != s_width || h != s_height) {
s_width = w;
s_height = h;
m_scale.x = (float)s_width / SCREEN_WIDTH;
m_scale.y = (float)s_height / SCREEN_HEIGHT;
this->createFramebufferResources();
renderer->UpdateViewSize(s_width, s_height);
}
this->Poll();
this->Update();
this->Draw();
}
}
auto App::Push(std::shared_ptr<ui::Widget> widget) -> void {
log_write("[Mui] pushing widget\n");
if (!g_app->m_widgets.empty()) {
g_app->m_widgets.back()->OnFocusLost();
}
log_write("doing focus gained\n");
g_app->m_widgets.emplace_back(widget)->OnFocusGained();
log_write("did it\n");
}
void App::Notify(std::string text, ui::NotifEntry::Side side) {
g_app->m_notif_manager.Push({text, side});
}
void App::Notify(ui::NotifEntry entry) {
g_app->m_notif_manager.Push(entry);
}
void App::NotifyPop(ui::NotifEntry::Side side) {
g_app->m_notif_manager.Pop(side);
}
void App::NotifyClear(ui::NotifEntry::Side side) {
g_app->m_notif_manager.Clear(side);
}
void App::NotifyFlashLed() {
static const HidsysNotificationLedPattern pattern = {
.baseMiniCycleDuration = 0x1, // 12.5ms.
.totalMiniCycles = 0x1, // 1 mini cycle(s).
.totalFullCycles = 0x1, // 1 full run(s).
.startIntensity = 0xF, // 100%.
.miniCycles = {{
.ledIntensity = 0xF, // 100%.
.transitionSteps = 0xF, // 1 step(s). Total 12.5ms.
.finalStepDuration = 0xF, // Forced 12.5ms.
}}
};
s32 total;
HidsysUniquePadId unique_pad_ids[16] = {0};
if (R_SUCCEEDED(hidsysGetUniquePadIds(unique_pad_ids, 16, &total))) {
for (int i = 0; i < total; i++) {
hidsysSetNotificationLedPattern(&pattern, unique_pad_ids[i]);
}
}
}
auto App::GetThemeMetaList() -> std::span<ThemeMeta> {
return g_app->m_theme_meta_entries;
}
void App::SetTheme(u64 theme_index) {
g_app->LoadTheme(g_app->m_theme_meta_entries[theme_index].ini_path.c_str());
g_app->m_theme_index = theme_index;
}
auto App::GetThemeIndex() -> u64 {
return g_app->m_theme_index;
}
auto App::GetExePath() -> fs::FsPath {
return g_app->m_app_path;
}
auto App::IsHbmenu() -> bool {
return !strcasecmp(GetExePath().s, "/hbmenu.nro");
}
auto App::GetNxlinkEnable() -> bool {
return g_app->m_nxlink_enabled.Get();
}
auto App::GetLogEnable() -> bool {
return g_app->m_log_enabled.Get();
}
auto App::GetReplaceHbmenuEnable() -> bool {
return g_app->m_replace_hbmenu.Get();
}
auto App::GetInstallEnable() -> bool {
return g_app->m_install.Get();
}
auto App::GetInstallSdEnable() -> bool {
return g_app->m_install_sd.Get();
}
auto App::GetInstallPrompt() -> bool {
return g_app->m_install_prompt.Get();
}
auto App::GetThemeShuffleEnable() -> bool {
return g_app->m_theme_shuffle.Get();
}
auto App::GetThemeMusicEnable() -> bool {
return g_app->m_theme_music.Get();
}
auto App::GetMtpEnable() -> bool {
return g_app->m_mtp_enabled.Get();
}
auto App::GetFtpEnable() -> bool {
return g_app->m_ftp_enabled.Get();
}
auto App::GetLanguage() -> long {
return g_app->m_language.Get();
}
void App::SetNxlinkEnable(bool enable) {
if (App::GetNxlinkEnable() != enable) {
g_app->m_nxlink_enabled.Set(enable);
if (enable) {
nxlinkInitialize(nxlink_callback);
} else {
nxlinkExit();
}
}
}
void App::SetLogEnable(bool enable) {
if (App::GetLogEnable() != enable) {
g_app->m_log_enabled.Set(enable);
if (enable) {
log_file_init();
} else {
log_file_exit();
}
}
}
void App::SetReplaceHbmenuEnable(bool enable) {
g_app->m_replace_hbmenu.Set(enable);
}
void App::SetInstallEnable(bool enable) {
g_app->m_install.Set(enable);
}
void App::SetInstallSdEnable(bool enable) {
g_app->m_install_sd.Set(enable);
}
void App::SetInstallPrompt(bool enable) {
g_app->m_install_prompt.Set(enable);
}
void App::SetThemeShuffleEnable(bool enable) {
g_app->m_theme_shuffle.Set(enable);
}
void App::SetThemeMusicEnable(bool enable) {
g_app->m_theme_music.Set(enable);
PlaySoundEffect(SoundEffect::SoundEffect_Music);
}
void App::SetMtpEnable(bool enable) {
if (App::GetMtpEnable() != enable) {
g_app->m_mtp_enabled.Set(enable);
if (enable) {
hazeInitialize(haze_callback);
} else {
hazeExit();
}
}
}
void App::SetFtpEnable(bool enable) {
if (App::GetFtpEnable() != enable) {
g_app->m_ftp_enabled.Set(enable);
if (enable) {
ftpsrv::Init();
} else {
ftpsrv::Exit();
}
}
}
void App::SetLanguage(long index) {
if (App::GetLanguage() != index) {
g_app->m_language.Set(index);
on_i18n_change();
}
}
auto App::Install(OwoConfig& config) -> Result {
R_TRY(romfsInit());
ON_SCOPE_EXIT(romfsExit());
std::vector<u8> main_data, npdm_data, logo_data, gif_data;
R_TRY(fs::read_entire_file("romfs:/exefs/main", main_data));
R_TRY(fs::read_entire_file("romfs:/exefs/main.npdm", npdm_data));
config.nro_path = nro_add_arg_file(config.nro_path);
config.main = main_data;
config.npdm = npdm_data;
config.logo = logo_data;
config.gif = gif_data;
if (!config.icon.empty()) {
config.icon = GetNroIcon(config.icon);
}
const auto rc = install_forwarder(config, App::GetInstallSdEnable() ? NcmStorageId_SdCard : NcmStorageId_BuiltInUser);
if (R_FAILED(rc)) {
App::PlaySoundEffect(SoundEffect_Error);
App::Push(std::make_shared<ui::ErrorBox>(rc, "Failed to install forwarder"_i18n));
} else {
App::PlaySoundEffect(SoundEffect_Install);
App::Notify("Installed!"_i18n);
}
return rc;
}
auto App::Install(ui::ProgressBox* pbox, OwoConfig& config) -> Result {
R_TRY(romfsInit());
ON_SCOPE_EXIT(romfsExit());
std::vector<u8> main_data, npdm_data, logo_data, gif_data;
R_TRY(fs::read_entire_file("romfs:/exefs/main", main_data));
R_TRY(fs::read_entire_file("romfs:/exefs/main.npdm", npdm_data));
config.nro_path = nro_add_arg_file(config.nro_path);
config.main = main_data;
config.npdm = npdm_data;
config.logo = logo_data;
config.gif = gif_data;
if (!config.icon.empty()) {
config.icon = GetNroIcon(config.icon);
}
const auto rc = install_forwarder(pbox, config, GetInstallSdEnable() ? NcmStorageId_SdCard : NcmStorageId_BuiltInUser);
if (R_FAILED(rc)) {
App::PlaySoundEffect(SoundEffect_Error);
App::Push(std::make_shared<ui::ErrorBox>(rc, "Failed to install forwarder"_i18n));
} else {
App::PlaySoundEffect(SoundEffect_Install);
App::Notify("Installed!"_i18n);
}
return rc;
}
void App::Exit() {
g_app->m_quit = true;
}
void App::ExitRestart() {
nro_launch(GetExePath());
Exit();
}
void App::Poll() {
m_controller.Reset();
padUpdate(&m_pad);
m_controller.m_kdown = padGetButtonsDown(&m_pad);
m_controller.m_kheld = padGetButtons(&m_pad);
m_controller.m_kup = padGetButtonsUp(&m_pad);
// dpad
m_controller.UpdateButtonHeld(HidNpadButton_Left);
m_controller.UpdateButtonHeld(HidNpadButton_Right);
m_controller.UpdateButtonHeld(HidNpadButton_Down);
m_controller.UpdateButtonHeld(HidNpadButton_Up);
// ls
m_controller.UpdateButtonHeld(HidNpadButton_StickLLeft);
m_controller.UpdateButtonHeld(HidNpadButton_StickLRight);
m_controller.UpdateButtonHeld(HidNpadButton_StickLDown);
m_controller.UpdateButtonHeld(HidNpadButton_StickLUp);
// rs
m_controller.UpdateButtonHeld(HidNpadButton_StickRLeft);
m_controller.UpdateButtonHeld(HidNpadButton_StickRRight);
m_controller.UpdateButtonHeld(HidNpadButton_StickRDown);
m_controller.UpdateButtonHeld(HidNpadButton_StickRUp);
HidTouchScreenState touch_state{};
hidGetTouchScreenStates(&touch_state, 1);
if (touch_state.count == 1 && !m_touch_info.is_touching) {
m_touch_info.initial_x = m_touch_info.prev_x = m_touch_info.cur_x = touch_state.touches[0].x;
m_touch_info.initial_y = m_touch_info.prev_y = m_touch_info.cur_y = touch_state.touches[0].y;
m_touch_info.finger_id = touch_state.touches[0].finger_id;
m_touch_info.is_touching = true;
m_touch_info.is_tap = true;
// PlaySoundEffect(SoundEffect_Limit);
} else if (touch_state.count >= 1 && m_touch_info.is_touching && m_touch_info.finger_id == touch_state.touches[0].finger_id) {
m_touch_info.prev_x = m_touch_info.cur_x;
m_touch_info.prev_y = m_touch_info.cur_y;
m_touch_info.cur_x = touch_state.touches[0].x;
m_touch_info.cur_y = touch_state.touches[0].y;
if (m_touch_info.is_tap &&
(std::abs(m_touch_info.initial_x - m_touch_info.cur_x) > 20 ||
std::abs(m_touch_info.initial_y - m_touch_info.cur_y) > 20)) {
m_touch_info.is_tap = false;
}
} else if (m_touch_info.is_touching) {
m_touch_info.is_touching = false;
// check if we clicked on anything, if so, handle it
if (m_touch_info.is_tap) {
// todo:
}
}
}
void App::Update() {
m_widgets.back()->Update(&m_controller, &m_touch_info);
bool popped_at_least1 = false;
while (true) {
if (m_widgets.empty()) {
log_write("[Mui] no widgets left, so we exit...");
App::Exit();
return;
}
if (m_widgets.back()->ShouldPop()) {
log_write("popping widget\n");
m_widgets.pop_back();
popped_at_least1 = true;
} else {
break;
}
}
if (!m_widgets.empty() && popped_at_least1) {
m_widgets.back()->OnFocusGained();
}
}
void App::Draw() {
const auto slot = this->queue.acquireImage(this->swapchain);
this->queue.submitCommands(this->framebuffer_cmdlists[slot]);
this->queue.submitCommands(this->render_cmdlist);
nvgBeginFrame(this->vg, s_width, s_height, 1.f);
nvgScale(vg, m_scale.x, m_scale.y);
// find the last menu in the list, start drawing from there
auto menu_it = m_widgets.rend();
for (auto it = m_widgets.rbegin(); it != m_widgets.rend(); it++) {
const auto& p = *it;
if (!p->IsHidden() && p->IsMenu()) {
menu_it = it;
break;
}
}
// reverse itr so loop backwards to go forwarders.
if (menu_it != m_widgets.rend()) {
for (auto it = menu_it; ; it--) {
const auto& p = *it;
// draw everything not hidden on top of the menu.
if (!p->IsHidden()) {
p->Draw(vg, &m_theme);
}
if (it == m_widgets.rbegin()) {
break;
}
}
}
m_notif_manager.Draw(vg, &m_theme);
nvgResetTransform(vg);
nvgEndFrame(this->vg);
this->queue.presentImage(this->swapchain, slot);
}
auto App::GetVg() -> NVGcontext* {
return g_app->vg;
}
void DrawElement(float x, float y, float w, float h, ThemeEntryID id) {
const auto& e = g_app->m_theme.elements[id];
switch (e.type) {
case ElementType::None: {
} break;
case ElementType::Texture: {
const auto paint = nvgImagePattern(g_app->vg, x, y, w, h, 0, e.texture, 1.f);
ui::gfx::drawRect(g_app->vg, x, y, w, h, paint);
} break;
case ElementType::Colour: {
ui::gfx::drawRect(g_app->vg, x, y, w, h, e.colour);
} break;
}
}
auto App::LoadElementImage(std::string_view value) -> ElementEntry {
ElementEntry entry{};
entry.texture = nvgCreateImage(vg, value.data(), 0);
if (entry.texture) {
entry.type = ElementType::Texture;
}
return entry;
}
auto App::LoadElementColour(std::string_view value) -> ElementEntry {
ElementEntry entry{};
if (value.starts_with("0x")) {
value = value.substr(2);
} else if (value.starts_with('#')) {
value = value.substr(1);
}
const u32 c = std::strtol(value.data(), nullptr, 16);
if (c) {
entry.colour = nvgRGBA((c >> 24) & 0xFF, (c >> 16) & 0xFF, (c >> 8) & 0xFF, c & 0xFF);
entry.type = ElementType::Colour;
}
return entry;
}
auto App::LoadElement(std::string_view value) -> ElementEntry {
if (value.size() <= 1) {
return {};
}
if (auto e = LoadElementImage(value); e.type != ElementType::None) {
return e;
}
if (auto e = LoadElementColour(value); e.type != ElementType::None) {
return e;
}
return {};
}
void App::CloseTheme() {
m_theme.name.clear();
m_theme.author.clear();
m_theme.version.clear();
m_theme.path.clear();
if (m_sound_ids[SoundEffect_Music]) {
plsrPlayerFree(m_sound_ids[SoundEffect_Music]);
m_sound_ids[SoundEffect_Music] = nullptr;
plsrBFSTMClose(&m_theme.music);
}
for (auto& e : m_theme.elements) {
if (e.type == ElementType::Texture) {
nvgDeleteImage(vg, e.texture);
}
e.type = ElementType::None;
}
}
void App::LoadTheme(const fs::FsPath& path) {
// reset theme
CloseTheme();
m_theme.path = path;
const auto cb = [](const mTCHAR *Section, const mTCHAR *Key, const mTCHAR *Value, void *UserData) -> int {
auto app = static_cast<App*>(UserData);
auto& theme = app->m_theme;
std::string_view section{Section};
std::string_view key{Key};
std::string_view value{Value};
if (section == "meta") {
if (key == "name") {
theme.name = key;
} else if (key == "author") {
theme.author = key;
} else if (key == "version") {
theme.version = key;
}
} else if (section == "theme") {
if (key == "background") {
theme.elements[ThemeEntryID_BACKGROUND] = app->LoadElement(value);
} else if (key == "music") {
if (R_SUCCEEDED(plsrBFSTMOpen(Value, &theme.music))) {
if (R_SUCCEEDED(plsrPlayerLoadStream(&theme.music, &app->m_sound_ids[SoundEffect_Music]))) {
app->PlaySoundEffect(SoundEffect_Music);
}
}
} else if (key == "grid") {
theme.elements[ThemeEntryID_GRID] = app->LoadElement(value);
} else if (key == "selected") {
theme.elements[ThemeEntryID_SELECTED] = app->LoadElement(value);
} else if (key == "selected_overlay") {
theme.elements[ThemeEntryID_SELECTED_OVERLAY] = app->LoadElement(value);
} else if (key == "text") {
theme.elements[ThemeEntryID_TEXT] = app->LoadElementColour(value);
} else if (key == "text_selected") {
theme.elements[ThemeEntryID_TEXT_SELECTED] = app->LoadElementColour(value);
} else if (key == "icon_audio") {
theme.elements[ThemeEntryID_ICON_AUDIO] = app->LoadElement(value);
} else if (key == "icon_video") {
theme.elements[ThemeEntryID_ICON_VIDEO] = app->LoadElement(value);
} else if (key == "icon_image") {
theme.elements[ThemeEntryID_ICON_IMAGE] = app->LoadElement(value);
} else if (key == "icon_file") {
theme.elements[ThemeEntryID_ICON_FILE] = app->LoadElement(value);
} else if (key == "icon_folder") {
theme.elements[ThemeEntryID_ICON_FOLDER] = app->LoadElement(value);
} else if (key == "icon_zip") {
theme.elements[ThemeEntryID_ICON_ZIP] = app->LoadElement(value);
} else if (key == "icon_game") {
theme.elements[ThemeEntryID_ICON_GAME] = app->LoadElement(value);
} else if (key == "icon_nro") {
theme.elements[ThemeEntryID_ICON_NRO] = app->LoadElement(value);
}
}
return 1;
};
if (R_SUCCEEDED(romfsInit())) {
ON_SCOPE_EXIT(romfsExit());
if (!ini_browse(cb, this, path)) {
log_write("failed to open ini: %s\n", path);
} else {
log_write("opened ini: %s\n", path);
}
}
}
// todo: only use opendir on if romfs, otherwise use native fs
void App::ScanThemes(const std::string& path) {
auto dir = opendir(path.c_str());
if (!dir) {
return;
}
ON_SCOPE_EXIT(closedir(dir));
while (auto d = readdir(dir)) {
if (d->d_name[0] == '.') {
continue;
}
if (d->d_type != DT_REG) {
continue;
}
const std::string name = d->d_name;
if (!name.ends_with(".ini")) {
continue;
}
const auto full_path = path + name;
if (!ini_haskey("meta", "name", full_path.c_str())) {
continue;
}
if (!ini_haskey("meta", "author", full_path.c_str())) {
continue;
}
if (!ini_haskey("meta", "version", full_path.c_str())) {
continue;
}
ThemeMeta meta{};
char buf[FS_MAX_PATH]{};
int len{};
len = ini_gets("meta", "name", "", buf, sizeof(buf) - 1, full_path.c_str());
if (len <= 1) {
continue;
}
meta.name = buf;
len = ini_gets("meta", "author", "", buf, sizeof(buf) - 1, full_path.c_str());
if (len <= 1) {
continue;
}
meta.author = buf;
len = ini_gets("meta", "version", "", buf, sizeof(buf) - 1, full_path.c_str());
if (len <= 1) {
continue;
}
meta.version = buf;
meta.ini_path = full_path;
m_theme_meta_entries.emplace_back(meta);
}
}
void App::ScanThemeEntries() {
// load from romfs first
if (R_SUCCEEDED(romfsInit())) {
ScanThemes("romfs:/themes/");
romfsExit();
}
// then load custom entries
ScanThemes("/config/sphaira/themes/");
}
App::App(const char* argv0) {
g_app = this;
m_start_timestamp = armGetSystemTick();
if (!std::strncmp(argv0, "sdmc:/", 6)) {
// memmove(path, path + 5, strlen(path)-5);
std::strncpy(m_app_path, argv0 + 5, std::strlen(argv0)-5);
} else {
m_app_path = argv0;
}
// set if we are hbmenu
if (IsHbmenu()) {
__nx_applet_exit_mode = 1;
}
fs::FsNativeSd fs;
fs.CreateDirectoryRecursively("/config/sphaira/assoc");
fs.CreateDirectoryRecursively("/config/sphaira/themes");
fs.CreateDirectoryRecursively("/config/sphaira/github");
fs.CreateDirectoryRecursively("/config/sphaira/i18n");
if (App::GetLogEnable()) {
log_file_init();
log_write("hello world\n");
}
if (App::GetMtpEnable()) {
hazeInitialize(haze_callback);
}
if (App::GetFtpEnable()) {
ftpsrv::Init();
}
if (App::GetNxlinkEnable()) {
nxlinkInitialize(nxlink_callback);
}
curl::Init();
curl::cache::init();
// Create the deko3d device
this->device = dk::DeviceMaker{}
.setCbDebug(deko3d_error_cb)
.create();
// Create the main queue
this->queue = dk::QueueMaker{this->device}
.setFlags(DkQueueFlags_Graphics)
.create();
// Create the memory pools
this->pool_images.emplace(device, DkMemBlockFlags_GpuCached | DkMemBlockFlags_Image, 16*1024*1024);
this->pool_code.emplace(device, DkMemBlockFlags_CpuUncached | DkMemBlockFlags_GpuCached | DkMemBlockFlags_Code, 128*1024);
this->pool_data.emplace(device, DkMemBlockFlags_CpuUncached | DkMemBlockFlags_GpuCached, 1*1024*1024);
// Create the static command buffer and feed it freshly allocated memory
this->cmdbuf = dk::CmdBufMaker{this->device}.create();
const CMemPool::Handle cmdmem = this->pool_data->allocate(this->StaticCmdSize);
this->cmdbuf.addMemory(cmdmem.getMemBlock(), cmdmem.getOffset(), cmdmem.getSize());
// Create the framebuffer resources
this->createFramebufferResources();
this->renderer.emplace(SCREEN_WIDTH, SCREEN_HEIGHT, this->device, this->queue, *this->pool_images, *this->pool_code, *this->pool_data);
this->vg = nvgCreateDk(&*this->renderer, NVG_ANTIALIAS | NVG_STENCIL_STROKES);
i18n::init(GetLanguage());
// not sure if these are meant to be deleted or not...
PlFontData font_standard, font_extended, font_lang;
plGetSharedFontByType(&font_standard, PlSharedFontType_Standard);
plGetSharedFontByType(&font_extended, PlSharedFontType_NintendoExt);
auto standard_font = nvgCreateFontMem(this->vg, "Standard", (unsigned char*)font_standard.address, font_standard.size, 0);
auto extended_font = nvgCreateFontMem(this->vg, "Extended", (unsigned char*)font_extended.address, font_extended.size, 0);
nvgAddFallbackFontId(this->vg, standard_font, extended_font);
constexpr PlSharedFontType lang_font[] = {
PlSharedFontType_ChineseSimplified,
PlSharedFontType_ExtChineseSimplified,
PlSharedFontType_ChineseTraditional,
PlSharedFontType_KO,
};
for (auto type : lang_font) {
if (R_SUCCEEDED(plGetSharedFontByType(&font_lang, type))) {
char name[32];
snprintf(name, sizeof(name), "Lang_%u", font_lang.type);
auto lang_font = nvgCreateFontMem(this->vg, name, (unsigned char*)font_lang.address, font_lang.size, 0);
nvgAddFallbackFontId(this->vg, standard_font, lang_font);
} else {
log_write("failed plGetSharedFontByType(%d)\n", type);
}
}
if (R_SUCCEEDED(romfsMountDataStorageFromProgram(0x0100000000001000, "qlaunch"))) {
ON_SCOPE_EXIT(romfsUnmount("qlaunch"));
plsrPlayerInit();
plsrBFSAROpen("qlaunch:/sound/qlaunch.bfsar", &m_qlaunch_bfsar);
plsrPlayerLoadSoundByName(&m_qlaunch_bfsar, "SeGameIconFocus", &m_sound_ids[SoundEffect_Focus]);
plsrPlayerLoadSoundByName(&m_qlaunch_bfsar, "SeGameIconScroll", &m_sound_ids[SoundEffect_Scroll]);
plsrPlayerLoadSoundByName(&m_qlaunch_bfsar, "SeGameIconLimit", &m_sound_ids[SoundEffect_Limit]);
plsrPlayerLoadSoundByName(&m_qlaunch_bfsar, "SeStartupMenu_game", &m_sound_ids[SoundEffect_Startup]);
plsrPlayerLoadSoundByName(&m_qlaunch_bfsar, "SeGameIconAdd", &m_sound_ids[SoundEffect_Install]);
plsrPlayerLoadSoundByName(&m_qlaunch_bfsar, "SeInsertError", &m_sound_ids[SoundEffect_Error]);
plsrPlayerSetVolume(m_sound_ids[SoundEffect_Limit], 2.0f);
plsrPlayerSetVolume(m_sound_ids[SoundEffect_Focus], 0.5f);
PlaySoundEffect(SoundEffect_Startup);
} else {
log_write("failed to mount romfs 0x0100000000001000\n");
}
ScanThemeEntries();
fs::FsPath theme_path{};
if (App::GetThemeShuffleEnable() && m_theme_meta_entries.size()) {
theme_path = m_theme_meta_entries[randomGet64() % m_theme_meta_entries.size()].ini_path;
} else {
ini_gets("config", "theme", "romfs:/themes/abyss_theme.ini", theme_path, sizeof(theme_path), CONFIG_PATH);
}
LoadTheme(theme_path);
// find theme index using the path of the theme.ini
for (u64 i = 0; i < m_theme_meta_entries.size(); i++) {
if (m_theme.path == m_theme_meta_entries[i].ini_path) {
m_theme_index = i;
break;
}
}
appletHook(&m_appletHookCookie, appplet_hook_calback, this);
hidInitializeTouchScreen();
padConfigureInput(8, HidNpadStyleSet_NpadStandard);
// padInitializeDefault(&m_pad);
padInitializeAny(&m_pad);
m_prev_timestamp = ini_getl("paths", "timestamp", 0, App::CONFIG_PATH);
const auto last_launch_path_size = ini_gets("paths", "last_launch_path", "", m_prev_last_launch, sizeof(m_prev_last_launch), App::CONFIG_PATH);
fs::FsPath last_launch_path;
if (last_launch_path_size) {
ini_gets("paths", "last_launch_path", "", last_launch_path, sizeof(last_launch_path), App::CONFIG_PATH);
}
ini_puts("paths", "last_launch_path", "", App::CONFIG_PATH);
const auto loader_info_size = envGetLoaderInfoSize();
if (loader_info_size) {
if (loader_info_size >= 8 && !std::memcmp(envGetLoaderInfo(), "sphaira", 7)) {
log_write("launching from sphaira created forwarder\n");
m_is_launched_via_sphaira_forwader = true;
} else {
log_write("launching from unknown forwader: %.*s size: %zu\n", loader_info_size, envGetLoaderInfo(), loader_info_size);
}
} else {
log_write("not launching from forwarder\n");
}
ini_putl(GetExePath(), "timestamp", m_start_timestamp, App::PLAYLOG_PATH);
const long old_launch_count = ini_getl(GetExePath(), "launch_count", 0, App::PLAYLOG_PATH);
ini_putl(GetExePath(), "launch_count", old_launch_count + 1, App::PLAYLOG_PATH);
s64 sd_free_space;
if (R_SUCCEEDED(fs.GetFreeSpace("/", &sd_free_space))) {
log_write("sd_free_space: %zd\n", sd_free_space);
}
s64 sd_total_space;
if (R_SUCCEEDED(fs.GetTotalSpace("/", &sd_total_space))) {
log_write("sd_total_space: %zd\n", sd_total_space);
}
App::Push(std::make_shared<ui::menu::main::MainMenu>());
log_write("finished app constructor\n");
}
void App::PlaySoundEffect(SoundEffect effect) {
// Stop and free the last loaded sound
const auto id = g_app->m_sound_ids[effect];
if (plsrPlayerIsPlaying(id)) {
plsrPlayerStop(id);
plsrPlayerWaitNextFrame();
}
if (effect == SoundEffect_Music && !App::GetThemeMusicEnable()) {
return;
}
plsrPlayerPlay(id);
}
App::~App() {
log_write("starting to exit\n");
i18n::exit();
curl::Exit();
curl::cache::exit();
// this has to be called before any cleanup to ensure the lifetime of
// nvg is still active as some widgets may need to free images.
m_widgets.clear();
appletUnhook(&m_appletHookCookie);
ini_puts("config", "theme", m_theme.path, CONFIG_PATH);
CloseTheme();
// Free any loaded sound from memory
for (auto id : m_sound_ids) {
if (id) {
plsrPlayerFree(id);
}
}
// Close the archive
plsrBFSARClose(&m_qlaunch_bfsar);
// De-initialize our player
plsrPlayerExit();
this->destroyFramebufferResources();
nvgDeleteDk(this->vg);
this->renderer.reset();
// backup hbmenu if it is not sphaira
if (App::GetReplaceHbmenuEnable() && !IsHbmenu()) {
NacpStruct nacp;
fs::FsNativeSd fs;
if (R_SUCCEEDED(nro_get_nacp("/hbmenu.nro", nacp)) && std::strcmp(nacp.lang[0].name, "sphaira")) {
log_write("backing up hbmenu.nro\n");
if (R_FAILED(fs.copy_entire_file("/switch/hbmenu.nro", "/hbmenu.nro", true))) {
log_write("failed to backup hbmenu.nro\n");
}
} else {
log_write("not backing up\n");
}
Result rc;
if (R_FAILED(rc = fs.copy_entire_file("/hbmenu.nro", GetExePath(), true))) {
log_write("failed to copy entire file: %s 0x%X module: %u desc: %u\n", GetExePath(), rc, R_MODULE(rc), R_DESCRIPTION(rc));
} else {
log_write("success with copying over root file!\n");
}
} else if (IsHbmenu()) {
// check we have a version that's newer than current.
fs::FsNativeSd fs;
NacpStruct sphaira_nacp;
fs::FsPath sphaira_path = "/switch/sphaira/sphaira.nro";
Result rc;
rc = nro_get_nacp(sphaira_path, sphaira_nacp);
if (R_FAILED(rc) || std::strcmp(sphaira_nacp.lang[0].name, "sphaira")) {
sphaira_path = "/switch/sphaira.nro";
rc = nro_get_nacp(sphaira_path, sphaira_nacp);
}
// found sphaira, now lets get compare version
if (R_SUCCEEDED(rc) && !std::strcmp(sphaira_nacp.lang[0].name, "sphaira")) {
if (std::strcmp(APP_VERSION, sphaira_nacp.display_version) < 0) {
if (R_FAILED(rc = fs.copy_entire_file(GetExePath(), sphaira_path, true))) {
log_write("failed to copy entire file: %s 0x%X module: %u desc: %u\n", sphaira_path, rc, R_MODULE(rc), R_DESCRIPTION(rc));
} else {
log_write("success with updating hbmenu!\n");
}
}
}
}
if (App::GetMtpEnable()) {
log_write("closing mtp\n");
hazeExit();
}
if (App::GetFtpEnable()) {
log_write("closing ftp\n");
ftpsrv::Exit();
}
if (App::GetNxlinkEnable()) {
log_write("closing nxlink\n");
nxlinkExit();
}
if (App::GetLogEnable()) {
log_write("closing log\n");
log_file_exit();
}
u64 timestamp;
timeGetCurrentTime(TimeType_LocalSystemClock, &timestamp);
ini_putl("paths", "timestamp", timestamp, App::CONFIG_PATH);
}
void App::createFramebufferResources() {
this->swapchain = nullptr;
// Create layout for the depth buffer
dk::ImageLayout layout_depthbuffer;
dk::ImageLayoutMaker{device}
.setFlags(DkImageFlags_UsageRender | DkImageFlags_HwCompression)
.setFormat(DkImageFormat_S8)
.setDimensions(s_width, s_height)
.initialize(layout_depthbuffer);
// Create the depth buffer
this->depthBuffer_mem = this->pool_images->allocate(layout_depthbuffer.getSize(), layout_depthbuffer.getAlignment());
this->depthBuffer.initialize(layout_depthbuffer, this->depthBuffer_mem.getMemBlock(), this->depthBuffer_mem.getOffset());
// Create layout for the framebuffers
dk::ImageLayout layout_framebuffer;
dk::ImageLayoutMaker{device}
.setFlags(DkImageFlags_UsageRender | DkImageFlags_UsagePresent | DkImageFlags_HwCompression)
.setFormat(DkImageFormat_RGBA8_Unorm)
.setDimensions(s_width, s_height)
.initialize(layout_framebuffer);
// Create the framebuffers
std::array<DkImage const*, NumFramebuffers> fb_array;
const u64 fb_size = layout_framebuffer.getSize();
const uint32_t fb_align = layout_framebuffer.getAlignment();
for (unsigned i = 0; i < fb_array.size(); i++) {
// Allocate a framebuffer
this->framebuffers_mem[i] = pool_images->allocate(fb_size, fb_align);
this->framebuffers[i].initialize(layout_framebuffer, framebuffers_mem[i].getMemBlock(), framebuffers_mem[i].getOffset());
// Generate a command list that binds it
dk::ImageView colorTarget{ framebuffers[i] }, depthTarget{ depthBuffer };
this->cmdbuf.bindRenderTargets(&colorTarget, &depthTarget);
this->framebuffer_cmdlists[i] = cmdbuf.finishList();
// Fill in the array for use later by the swapchain creation code
fb_array[i] = &framebuffers[i];
}
// Create the swapchain using the framebuffers
this->swapchain = dk::SwapchainMaker{device, nwindowGetDefault(), fb_array}.create();
// Generate the main rendering cmdlist
this->recordStaticCommands();
}
void App::destroyFramebufferResources() {
// Return early if we have nothing to destroy
if (!this->swapchain) {
return;
}
this->queue.waitIdle();
this->cmdbuf.clear();
swapchain.destroy();
// Destroy the framebuffers
for (unsigned i = 0; i < NumFramebuffers; i++) {
framebuffers_mem[i].destroy();
}
// Destroy the depth buffer
this->depthBuffer_mem.destroy();
}
void App::recordStaticCommands() {
// Initialize state structs with deko3d defaults
dk::RasterizerState rasterizerState;
dk::ColorState colorState;
dk::ColorWriteState colorWriteState;
dk::BlendState blendState;
// Configure the viewport and scissor
this->cmdbuf.setViewports(0, { { 0.0f, 0.0f, (float)s_width, (float)s_height, 0.0f, 1.0f } });
this->cmdbuf.setScissors(0, { { 0, 0, (u32)s_width, (u32)s_height } });
// Clear the color and depth buffers
this->cmdbuf.clearColor(0, DkColorMask_RGBA, 0.2f, 0.3f, 0.3f, 1.0f);
this->cmdbuf.clearDepthStencil(true, 1.0f, 0xFF, 0);
// Bind required state
this->cmdbuf.bindRasterizerState(rasterizerState);
this->cmdbuf.bindColorState(colorState);
this->cmdbuf.bindColorWriteState(colorWriteState);
this->render_cmdlist = this->cmdbuf.finishList();
}
} // namespace sphaira