Files
sphaira/sphaira/source/app.cpp

1180 lines
38 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 <nanovg_dk.h>
#include <minIni.h>
#include <pulsar.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!");
break;
case AppletOperationMode_Console:
log_write("[APPLET] AppletOperationMode_Console\n");
App::Notify("Switch-Docked!");
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 nxlink_callback(const NxlinkCallbackData *data) {
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();
auto events = evman::popall();
// while (auto e = evman::pop()) {
for (auto& e : events) {
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, NxlinkCallbackData>) {
switch (arg.type) {
case NxlinkCallbackType_Connected:
log_write("[NxlinkCallbackType_Connected]\n");
App::Notify("Nxlink Connected");
break;
case NxlinkCallbackType_WriteBegin:
log_write("[NxlinkCallbackType_WriteBegin] %s\n", arg.file.filename);
App::Notify("Nxlink Upload");
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");
break;
}
} else if constexpr(std::is_same_v<T, DownloadEventData>) {
log_write("[DownloadEventData] got event\n");
arg.callback(arg.data, arg.result);
} else {
static_assert(false, "non-exhaustive visitor!");
}
}, e);
}
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);
}
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::GetInstallSdEnable() -> bool {
return g_app->m_install_sd.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::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::SetInstallSdEnable(bool enable) {
g_app->m_install_sd.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::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"));
} else {
App::PlaySoundEffect(SoundEffect_Install);
App::Notify("Installed!");
}
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"));
} else {
App::PlaySoundEffect(SoundEffect_Install);
App::Notify("Installed!");
}
return rc;
}
void App::Exit() {
g_app->m_quit = true;
}
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);
// NOTE: widgets should never pop themselves from drawing!
for (auto& p : m_widgets) {
if (!p->IsHidden()) {
p->Draw(vg, &m_theme);
}
}
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;
}
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");
if (App::GetLogEnable()) {
log_file_init();
log_write("hello world\n");
}
if (App::GetNxlinkEnable()) {
nxlinkInitialize(nxlink_callback);
}
DownloadInit();
// 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();
DownloadExit();
// 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\n");
if (R_FAILED(fs.copy_entire_file("/switch/hbmenu.nro", "/hbmenu.nro", true))) {
log_write("failed to copy sphaire.nro to 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::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