Changes, add IPC code, start with themes

This commit is contained in:
XorTroll 2019-10-11 17:43:17 +02:00
parent 0340098246
commit 5eaf42fe5e
21 changed files with 317 additions and 216 deletions

View file

@ -20,4 +20,5 @@ namespace am
Result ApplicationStart(u64 app_id, bool system, u128 user_id); Result ApplicationStart(u64 app_id, bool system, u128 user_id);
bool ApplicationHasForeground(); bool ApplicationHasForeground();
Result ApplicationSetForeground(); Result ApplicationSetForeground();
u64 ApplicationGetId();
} }

View file

@ -25,7 +25,7 @@ namespace am
LaunchApplication, LaunchApplication,
ResumeApplication, ResumeApplication,
TerminateApplication, TerminateApplication,
GetLatestMessage, GetSuspendedApplicationId
}; };
Result QDaemon_LaunchQMenu(QMenuStartMode mode); Result QDaemon_LaunchQMenu(QMenuStartMode mode);

View file

@ -14,6 +14,8 @@ namespace fs
void DeleteDirectory(std::string path); void DeleteDirectory(std::string path);
void DeleteFile(std::string path); void DeleteFile(std::string path);
bool WriteFile(std::string path, void *data, size_t size, bool overwrite);
bool ReadFile(std::string path, void *data, size_t size);
size_t GetFileSize(std::string path); size_t GetFileSize(std::string path);
void ForEachFileIn(std::string dir, std::function<void(std::string name, std::string path)> fn); void ForEachFileIn(std::string dir, std::function<void(std::string name, std::string path)> fn);

View file

@ -22,7 +22,8 @@ using JSON = nlohmann::json;
#define Q_BASE_DIR "reqwrite" #define Q_BASE_DIR "reqwrite"
#define Q_BASE_SD_DIR "sdmc:/" Q_BASE_DIR #define Q_BASE_SD_DIR "sdmc:/" Q_BASE_DIR
#define Q_DB_MOUNT_NAME "qsave" #define Q_DB_MOUNT_NAME "qsave"
#define Q_BASE_DB_DIR Q_DB_MOUNT_NAME ":/" Q_BASE_DIR #define Q_DB_MOUNT_PATH Q_DB_MOUNT_NAME ":/"
#define Q_BASE_DB_DIR Q_DB_MOUNT_PATH Q_BASE_DIR
#define Q_MENU_JSON Q_BASE_SD_DIR "/menu.json" #define Q_MENU_JSON Q_BASE_SD_DIR "/menu.json"

View file

@ -4,7 +4,7 @@ namespace am
{ {
extern bool home_focus; extern bool home_focus;
AppletApplication app_holder; AppletApplication app_holder;
bool launched; u64 latest_appid;
bool ApplicationIsActive() bool ApplicationIsActive()
{ {
@ -45,6 +45,8 @@ namespace am
R_TRY(appletApplicationStart(&app_holder)); R_TRY(appletApplicationStart(&app_holder));
R_TRY(ApplicationSetForeground()); R_TRY(ApplicationSetForeground());
latest_appid = app_id;
return 0; return 0;
} }
@ -59,4 +61,9 @@ namespace am
if(R_SUCCEEDED(rc)) home_focus = false; if(R_SUCCEEDED(rc)) home_focus = false;
return rc; return rc;
} }
u64 ApplicationGetId()
{
return latest_appid;
}
} }

View file

@ -3,6 +3,7 @@
#include <util/util_JSON.hpp> #include <util/util_JSON.hpp>
#include <util/util_String.hpp> #include <util/util_String.hpp>
#include <fs/fs_Stdio.hpp> #include <fs/fs_Stdio.hpp>
#include <db/db_Save.hpp>
#include <util/util_Convert.hpp> #include <util/util_Convert.hpp>
namespace cfg namespace cfg
@ -160,13 +161,7 @@ namespace cfg
fseek(f, hdr.size + ahdr.icon.size, SEEK_SET); fseek(f, hdr.size + ahdr.icon.size, SEEK_SET);
if(fread(iconbuf, ahdr.icon.size, 1, f) == 1) if(fread(iconbuf, ahdr.icon.size, 1, f) == 1)
{ {
fs::DeleteFile(nroimg); fs::WriteFile(nroimg, iconbuf, ahdr.icon.size, true);
FILE *iconf = fopen(nroimg.c_str(), "wb");
if(iconf)
{
fwrite(iconbuf, 1, ahdr.icon.size, iconf);
fclose(iconf);
}
} }
delete[] iconbuf; delete[] iconbuf;
} }
@ -200,108 +195,6 @@ namespace cfg
} }
} }
} }
// Search SD card for installed title extensions or homebrew accessors
fs::ForEachFileIn(Q_BASE_SD_DIR "/entries", [&](std::string name, std::string path)
{
if(util::StringEndsWith(name, ".json"))
{
auto [rc, json] = util::LoadJSONFromFile(path);
if(R_SUCCEEDED(rc))
{
TitleType type = (TitleType)json.value("type", 0u);
if(type == TitleType::Installed)
{
std::string appidstr = json.value("application_id", "");
if(!appidstr.empty())
{
std::string folder = json.value("folder", "");
u64 appid = util::Get64FromString(appidstr);
if(appid > 0)
{
if(!folder.empty()) MoveTitleToDirectory(list, appid, folder);
}
}
}
else if(type == TitleType::Homebrew)
{
std::string nropath = json.value("nro_path", "");
if(!nropath.empty())
{
TitleRecord rec = {};
rec.title_type = (u32)type;
rec.nro_path = "sdmc:";
if(nropath.front() != '/') rec.nro_path += "/";
rec.nro_path += nropath;
std::string folder = json.value("folder", "");
rec.sub_folder = folder;
if(cache)
{
auto nroimg = GetNROCacheIconPath(rec.nro_path);
FILE *f = fopen(rec.nro_path.c_str(), "rb");
if(f)
{
NroStart st = {};
if(fread(&st, sizeof(NroStart), 1, f) == 1)
{
NroHeader hdr = {};
if(fread(&hdr, sizeof(NroHeader), 1, f) == 1)
{
fseek(f, hdr.size, SEEK_SET);
NroAssetHeader ahdr = {};
if(fread(&ahdr, sizeof(NroAssetHeader), 1, f) == 1)
{
if(ahdr.magic == NROASSETHEADER_MAGIC)
{
if(ahdr.icon.size > 0)
{
u8 *iconbuf = new u8[ahdr.icon.size]();
fseek(f, hdr.size + ahdr.icon.size, SEEK_SET);
if(fread(iconbuf, ahdr.icon.size, 1, f) == 1)
{
fs::DeleteFile(nroimg);
FILE *iconf = fopen(nroimg.c_str(), "wb");
if(iconf)
{
fwrite(iconbuf, 1, ahdr.icon.size, iconf);
fclose(iconf);
}
}
delete[] iconbuf;
}
}
}
}
}
fclose(f);
}
}
if(folder.empty())
{
list.root.titles.push_back(rec);
}
else
{
auto find = STLITER_FINDWITHCONDITION(list.folders, fld, (fld.name == folder));
if(STLITER_ISFOUND(list.folders, find))
{
STLITER_UNWRAP(find).titles.push_back(rec);
}
else
{
TitleFolder fld = {};
fld.name = folder;
fld.titles.push_back(rec);
list.folders.push_back(fld);
}
}
}
}
}
}
});
return SuccessResultWith(list); return SuccessResultWith(list);
} }

View file

@ -33,13 +33,7 @@ namespace db
auto filename = GetUserPasswordFilePath(user_id); auto filename = GetUserPasswordFilePath(user_id);
if(fs::ExistsFile(filename)) if(fs::ExistsFile(filename))
{ {
FILE *f = fopen(filename.c_str(), "rb"); if(fs::ReadFile(filename, &pb, sizeof(pb))) return SuccessResultWith(pb);
if(f)
{
fread(&pb, 1, sizeof(PassBlock), f);
fclose(f);
return SuccessResultWith(pb);
}
} }
return MakeResultWith(0xdead, pb); return MakeResultWith(0xdead, pb);
} }
@ -54,30 +48,16 @@ namespace db
{ {
std::string pwd; std::string pwd;
auto filename = GetUserPasswordFilePath(user_id); auto filename = GetUserPasswordFilePath(user_id);
if(fs::ExistsFile(filename)) if(fs::ExistsFile(filename)) return 0xdead;
{
FILE *f = fopen(filename.c_str(), "rb");
if(f)
{
fclose(f);
return 0xdead;
}
}
fs::DeleteFile(filename);
FILE *f = fopen(filename.c_str(), "wb");
if(f)
{
if((password.length() > 15) || (password.empty())) return 0xdead1; if((password.length() > 15) || (password.empty())) return 0xdead1;
PassBlock pb = {}; PassBlock pb = {};
memcpy(&pb.uid, &user_id, sizeof(u128)); memcpy(&pb.uid, &user_id, sizeof(u128));
char tmppass[0x10] = {0}; char tmppass[0x10] = {0};
strcpy(tmppass, password.c_str()); strcpy(tmppass, password.c_str());
sha256CalculateHash(pb.pass_sha, tmppass, 0x10); sha256CalculateHash(pb.pass_sha, tmppass, 0x10);
fwrite(&pb, 1, sizeof(PassBlock), f);
fclose(f); if(!fs::WriteFile(filename, &pb, sizeof(pb), true)) return 0xdead2;
Commit();
}
else return 0xdead2;
return 0; return 0;
} }

View file

@ -1,7 +1,11 @@
#include <fs/fs_Stdio.hpp> #include <fs/fs_Stdio.hpp>
#include <util/util_String.hpp>
#include <db/db_Save.hpp>
namespace fs namespace fs
{ {
#define FS_COMMIT_IF_DB_PATH(path) db::Commit();
static bool ExistsImpl(size_t st_mode, std::string path) static bool ExistsImpl(size_t st_mode, std::string path)
{ {
struct stat st; struct stat st;
@ -21,26 +25,56 @@ namespace fs
void CreateDirectory(std::string path) void CreateDirectory(std::string path)
{ {
mkdir(path.c_str(), 777); mkdir(path.c_str(), 777);
FS_COMMIT_IF_DB_PATH(path)
} }
void CreateFile(std::string path) void CreateFile(std::string path)
{ {
fsdevCreateFile(path.c_str(), 0, 0); fsdevCreateFile(path.c_str(), 0, 0);
FS_COMMIT_IF_DB_PATH(path)
} }
void CreateConcatenationFile(std::string path) void CreateConcatenationFile(std::string path)
{ {
fsdevCreateFile(path.c_str(), 0, FS_CREATE_BIG_FILE); fsdevCreateFile(path.c_str(), 0, FS_CREATE_BIG_FILE);
FS_COMMIT_IF_DB_PATH(path)
} }
void DeleteDirectory(std::string path) void DeleteDirectory(std::string path)
{ {
fsdevDeleteDirectoryRecursively(path.c_str()); fsdevDeleteDirectoryRecursively(path.c_str());
FS_COMMIT_IF_DB_PATH(path)
} }
void DeleteFile(std::string path) void DeleteFile(std::string path)
{ {
remove(path.c_str()); remove(path.c_str());
FS_COMMIT_IF_DB_PATH(path)
}
bool WriteFile(std::string path, void *data, size_t size, bool overwrite)
{
FILE *f = fopen(path.c_str(), overwrite ? "wb" : "ab+");
if(f)
{
fwrite(data, 1, size, f);
fclose(f);
FS_COMMIT_IF_DB_PATH(path)
return true;
}
return false;
}
bool ReadFile(std::string path, void *data, size_t size)
{
FILE *f = fopen(path.c_str(), "rb");
if(f)
{
fread(data, 1, size, f);
fclose(f);
return true;
}
return false;
} }
size_t GetFileSize(std::string path) size_t GetFileSize(std::string path)
@ -86,6 +120,8 @@ namespace fs
void MoveFile(std::string p1, std::string p2) void MoveFile(std::string p1, std::string p2)
{ {
rename(p1.c_str(), p2.c_str()); rename(p1.c_str(), p2.c_str());
FS_COMMIT_IF_DB_PATH(p1)
FS_COMMIT_IF_DB_PATH(p2)
} }
void CopyFile(std::string p, std::string np) void CopyFile(std::string p, std::string np)
@ -111,6 +147,7 @@ namespace fs
} }
delete[] tmp; delete[] tmp;
fclose(outf); fclose(outf);
FS_COMMIT_IF_DB_PATH(np)
} }
fclose(inf); fclose(inf);
} }

View file

@ -38,15 +38,10 @@ namespace os
if(R_SUCCEEDED(rc)) if(R_SUCCEEDED(rc))
{ {
auto iconcache = GetIconCacheImagePath(uidbuf[i]); auto iconcache = GetIconCacheImagePath(uidbuf[i]);
fs::DeleteFile(iconcache); fs::WriteFile(iconcache, imgbuf, imgsz, true);
FILE *f = fopen(iconcache.c_str(), "wb");
if(f)
{
fwrite(imgbuf, 1, imgsz, f);
fclose(f);
}
} }
} }
accountProfileClose(&prof);
} }
} }
} }

View file

@ -25,12 +25,8 @@ namespace os
{ {
auto fname = cfg::GetTitleCacheIconPath(rec.app_id); auto fname = cfg::GetTitleCacheIconPath(rec.app_id);
fs::DeleteFile(fname); fs::DeleteFile(fname);
FILE *f = fopen(fname.c_str(), "wb"); db::Commit();
if(f) fs::WriteFile(fname, control.icon, sizeof(control.icon), true);
{
fwrite(control.icon, 1, sizeof(control.icon), f);
fclose(f);
}
} }
} }
titles.push_back(rec); titles.push_back(rec);

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

View file

@ -24,6 +24,12 @@ namespace qmenu
accountInitialize(); accountInitialize();
nsInitialize(); nsInitialize();
db::Mount(); db::Mount();
fs::CreateDirectory(Q_BASE_DB_DIR);
fs::CreateDirectory(Q_BASE_SD_DIR);
fs::CreateDirectory(Q_BASE_SD_DIR "/title");
fs::CreateDirectory(Q_BASE_SD_DIR "/user");
fs::CreateDirectory(Q_BASE_SD_DIR "/nro");
db::Commit();
am::QMenu_InitializeDaemonService(); am::QMenu_InitializeDaemonService();
} }
@ -38,25 +44,10 @@ namespace qmenu
u8 *app_buf; u8 *app_buf;
extern "C"
{
void userAppInit(void)
{
qmenu::Initialize();
}
void userAppExit(void)
{
delete[] app_buf;
qmenu::Exit();
}
}
int main() int main()
{ {
app_buf = new u8[1280 * 720 * 4](); app_buf = new u8[1280 * 720 * 4]();
fs::CreateDirectory(Q_BASE_DB_DIR); qmenu::Initialize();
fs::CreateDirectory(Q_BASE_DB_DIR "/user");
auto [_rc, menulist] = cfg::LoadTitleList(true); auto [_rc, menulist] = cfg::LoadTitleList(true);
list = menulist; list = menulist;
@ -70,10 +61,11 @@ int main()
if(smode == am::QMenuStartMode::MenuApplicationSuspended) if(smode == am::QMenuStartMode::MenuApplicationSuspended)
{ {
/* am::QMenuCommandWriter writer(am::QDaemonMessage::GetSuspendedApplicationId);
auto app_id = reader.Read<u64>(); writer.FinishWrite();
qapp->SetSuspendedApplicationId(app_id); am::QMenuCommandResultReader reader;
*/ if(reader) qapp->SetSuspendedApplicationId(reader.Read<u64>());
reader.FinishRead();
FILE *f = fopen(Q_BASE_SD_DIR "/temp-suspended.rgba", "rb"); FILE *f = fopen(Q_BASE_SD_DIR "/temp-suspended.rgba", "rb");
if(f) if(f)
@ -91,5 +83,8 @@ int main()
} }
} }
delete[] app_buf;
qmenu::Exit();
return 0; return 0;
} }

View file

@ -20,6 +20,10 @@ namespace ui
this->Add(this->bgSuspendedRaw); this->Add(this->bgSuspendedRaw);
this->itemsMenu = SideMenu::New(pu::ui::Color(0, 120, 255, 255), pu::ui::Color()); this->itemsMenu = SideMenu::New(pu::ui::Color(0, 120, 255, 255), pu::ui::Color());
// Add first item for all titles menu
this->itemsMenu->AddItem("romfs:/default/ui/AllTitles.png");
for(auto itm: list.root.titles) for(auto itm: list.root.titles)
{ {
std::string iconpth; std::string iconpth;
@ -29,7 +33,8 @@ namespace ui
} }
for(auto folder: list.folders) for(auto folder: list.folders)
{ {
this->itemsMenu->AddItem("romfs:/Test.jpg"); // TODO: folder logic
this->itemsMenu->AddItem("romfs:/default/Folder.png");
} }
this->itemsMenu->SetOnItemSelected(std::bind(&MenuLayout::menu_Click, this, std::placeholders::_1)); this->itemsMenu->SetOnItemSelected(std::bind(&MenuLayout::menu_Click, this, std::placeholders::_1));
this->Add(this->itemsMenu); this->Add(this->itemsMenu);
@ -39,9 +44,16 @@ namespace ui
void MenuLayout::menu_Click(u32 index) void MenuLayout::menu_Click(u32 index)
{ {
if(index < list.root.titles.size()) if(index == 0)
{ {
auto title = list.root.titles[index]; qapp->CreateShowDialog("A", "All titles", {"Ok"}, true);
}
else
{
u32 realidx = index - 1;
if(realidx < list.root.titles.size())
{
auto title = list.root.titles[realidx];
if(!qapp->IsTitleSuspended()) if(!qapp->IsTitleSuspended())
{ {
am::QMenuCommandWriter writer(am::QDaemonMessage::LaunchApplication); am::QMenuCommandWriter writer(am::QDaemonMessage::LaunchApplication);
@ -65,10 +77,11 @@ namespace ui
} }
else else
{ {
auto folder = list.folders[index - list.root.titles.size()]; auto folder = list.folders[realidx - list.root.titles.size()];
qapp->CreateShowDialog("Folder", folder.name, {"Ok"}, true); qapp->CreateShowDialog("Folder", folder.name, {"Ok"}, true);
} }
} }
}
void MenuLayout::OnInput(u64 down, u64 up, u64 held, pu::ui::Touch pos) void MenuLayout::OnInput(u64 down, u64 up, u64 held, pu::ui::Touch pos)
{ {
@ -88,19 +101,6 @@ namespace ui
break; break;
} }
/*
if(down & KEY_X)
{
auto [rc, msg] = am::QMenu_GetLatestQMenuMessage();
qapp->CreateShowDialog("HOME SWEET HOME", "0x" + util::FormatApplicationId((u32)msg), {"Ok"}, true);
if(qapp->IsTitleSuspended())
{
if(this->mode == 1) this->mode = 2;
}
else while(this->itemsMenu->GetSelectedItem() > 0) this->itemsMenu->HandleMoveLeft();
}
*/
if(this->susptr != NULL) if(this->susptr != NULL)
{ {

View file

@ -6,7 +6,7 @@ namespace ui
{ {
void QMenuApplication::OnLoad() void QMenuApplication::OnLoad()
{ {
pu::ui::render::SetDefaultFont("romfs:/Gilroy-Bold.ttf"); pu::ui::render::SetDefaultFont("romfs:/default/ui/Font.ttf");
this->startupLayout = StartupLayout::New(pu::ui::Color(10, 120, 255, 255)); this->startupLayout = StartupLayout::New(pu::ui::Color(10, 120, 255, 255));
this->menuLayout = MenuLayout::New(app_buf); this->menuLayout = MenuLayout::New(app_buf);

View file

@ -0,0 +1,28 @@
#pragma once
#include <q_Include.hpp>
#include <stratosphere.hpp>
namespace ipc
{
class IDaemonService : public IServiceObject
{
private:
enum class CommandId
{
GetLatestMessage = 0
};
public:
Result GetLatestMessage(Out<u32> msg);
public:
DEFINE_SERVICE_DISPATCH_TABLE
{
MAKE_SERVICE_COMMAND_META(IDaemonService, GetLatestMessage)
};
};
}

View file

@ -13,7 +13,7 @@
extern "C" extern "C"
{ {
u32 __nx_applet_type = AppletType_SystemApplet; u32 __nx_applet_type = AppletType_SystemApplet;
size_t __nx_heap_size = 0x1000000; size_t __nx_heap_size = 0x3000000;//0x1000000;
} }
void CommonSleepHandle() void CommonSleepHandle()
@ -187,6 +187,23 @@ void HandleQMenuMessage()
} }
break; break;
} }
case am::QDaemonMessage::GetSuspendedApplicationId:
{
reader.FinishRead();
if(am::ApplicationIsActive())
{
am::QDaemonCommandResultWriter res(0);
res.Write<u64>(am::ApplicationGetId());
res.FinishWrite();
}
else
{
am::QDaemonCommandResultWriter res(0xDEAD1);
res.FinishWrite();
}
break;
}
default: default:
break; break;
} }
@ -201,6 +218,53 @@ namespace qdaemon
app_buf = new u8[1280 * 720 * 4](); app_buf = new u8[1280 * 720 * 4]();
fs::CreateDirectory(Q_BASE_SD_DIR); fs::CreateDirectory(Q_BASE_SD_DIR);
// Debug testing mode
consoleInit(NULL);
CONSOLE_FMT("Welcome to QDaemon's debug mode!")
CONSOLE_FMT("")
CONSOLE_FMT("(A) -> Dump system save data to sd:/<q>/save_dump")
CONSOLE_FMT("(B) -> Delete everything in save data (except official HOME menu's content)")
CONSOLE_FMT("(X) -> Reboot system")
CONSOLE_FMT("(Y) -> Continue to QMenu (proceed launch)")
CONSOLE_FMT("")
while(true)
{
hidScanInput();
auto k = hidKeysDown(CONTROLLER_P1_AUTO);
if(k & KEY_A)
{
db::Mount();
fs::CopyDirectory(Q_DB_MOUNT_NAME ":/", Q_BASE_SD_DIR "/save_dump");
db::Unmount();
CONSOLE_FMT(" - Dump done.")
}
else if(k & KEY_B)
{
db::Mount();
fs::DeleteDirectory(Q_BASE_DB_DIR);
fs::CreateDirectory(Q_BASE_DB_DIR);
db::Commit();
db::Unmount();
CONSOLE_FMT(" - Cleanup done.")
}
else if(k & KEY_X)
{
CONSOLE_FMT(" - Rebooting...")
svcSleepThread(200'000'000);
appletStartRebootSequence();
}
else if(k & KEY_Y)
{
CONSOLE_FMT(" - Proceeding with launch...")
svcSleepThread(500'000'000);
break;
}
svcSleepThread(10'000'000);
}
consoleExit(NULL);
svcSleepThread(100'000'000); // Wait for proper moment svcSleepThread(100'000'000); // Wait for proper moment
} }

View file

@ -0,0 +1,16 @@
#include <ipc/ipc_IDaemonService.hpp>
#include <am/am_QCommunications.hpp>
extern HosMutex latestqlock;
extern am::QMenuMessage latestqmenumsg;
namespace ipc
{
Result IDaemonService::GetLatestMessage(Out<u32> msg)
{
std::scoped_lock lck(latestqlock);
msg.SetValue((u32)latestqmenumsg);
latestqmenumsg = am::QMenuMessage::Invalid;
return 0;
}
}

85
Themes.md Normal file
View file

@ -0,0 +1,85 @@
# Theme system
## **IMPORTANT!** Don't use this system yet. Wait until this warning is removed (when the system is finally decided)
> Current theme format version: none (unreleased yet)
Themes consist on plain directories replacing RomFs content.
A valid theme must, at least, contain a `/theme` subdirectory and a `/theme/Manifest.json` within it (icon or actual modifications aren't neccessary)
**Important note:** any content (sound or UI) not found in the theme will be loaded from the default config, thus there is no need to provide every file (for instance, when making UI-only or sound-only changes)
## Metadata
Metadata is stored inside `/theme` directory. It is required for the theme to be recognized as a valid one.
- `theme/Manifest.json` -> JSON containing theme information
Demo JSON:
```json
{
"name": "My awesome theme",
"version": 0,
"release": "0.1",
"description": "This is a really cool theme, check it out!",
"author": "XorTroll"
}
```
Properties:
- **name**: Theme name
- **version**: Theme format version (qlaunch updates might introduce changes to themes, thus a new format version would be out).
- **release**: Theme version string
- **description**: Theme description
- **author**: Theme author name(s)
- `theme/Icon(.png?)` -> Icon (which size and format shall we use?)
## Sound
Sound consists on custom *background music* and *sound effects* via files inside `/sound`.
- `sound/BGM.mp3` -> MP3 file to replace with custom music
- `sound/BGM.json` -> JSON file with BGM settings
Demo JSON:
```json
{
"loop": true,
"fade_in": true,
"fade_out": false
}
```
Properties:
- **loop**: Whether to replay the MP3 file again, after it finishes
- **fade_in**: Whether starting the music should apply a fade-in effect.
- **fade_out**: Whether stopping the music should apply a fade-out effect.
Note: returning to/launching a title/applet and returning back to HOME menu will restart the music.
> *TODO: Sound effects*
## UI
Can be customized via files in `/ui`.
- `ui/Font.ttf` -> TTF font used for all the UI.
- `ui/AllTitles.png` -> 256x256 icon for the "all titles" entry in the main menu.
- `ui/Folder.png` -> 256x256 icon for folders in the main menu.
> *TODO: more customizable stuff*

1
libstratosphere Submodule

@ -0,0 +1 @@
Subproject commit 8bae7b4a78f8fe5061596f354e38ecd0238cdaed