diff --git a/sphaira/CMakeLists.txt b/sphaira/CMakeLists.txt index 0339ef9..2fb5d02 100644 --- a/sphaira/CMakeLists.txt +++ b/sphaira/CMakeLists.txt @@ -47,6 +47,7 @@ add_executable(sphaira source/ui/menus/themezer.cpp source/ui/menus/ghdl.cpp source/ui/menus/usb_menu.cpp + source/ui/menus/gc_menu.cpp source/ui/error_box.cpp source/ui/notification.cpp diff --git a/sphaira/include/fs.hpp b/sphaira/include/fs.hpp index 07b6528..8f81c01 100644 --- a/sphaira/include/fs.hpp +++ b/sphaira/include/fs.hpp @@ -454,4 +454,10 @@ struct FsNativeContentStorage final : FsNative { } }; +struct FsNativeGameCard final : FsNative { + FsNativeGameCard(const FsGameCardHandle* handle, FsGameCardPartition partition, bool ignore_read_only = true) : FsNative{ignore_read_only} { + m_open_result = fsOpenGameCardFileSystem(&m_fs, handle, partition); + } +}; + } // namespace fs diff --git a/sphaira/include/ui/menus/gc_menu.hpp b/sphaira/include/ui/menus/gc_menu.hpp new file mode 100644 index 0000000..49af9a6 --- /dev/null +++ b/sphaira/include/ui/menus/gc_menu.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include "ui/menus/menu_base.hpp" +#include "yati/container/base.hpp" +#include "yati/source/base.hpp" + +namespace sphaira::ui::menu::gc { + +enum class State { + // no gamecard inserted. + None, + // set whilst transfer is in progress. + Progress, + // set when the transfer is finished. + Done, + // set when no gamecard is inserted. + NotFound, + // failed to parse gamecard. + Failed, +}; + +struct Menu final : MenuBase { + Menu(); + ~Menu(); + + void Update(Controller* controller, TouchInfo* touch) override; + void Draw(NVGcontext* vg, Theme* theme) override; + + Result ScanGamecard(); + +private: + std::unique_ptr m_fs{}; + FsDeviceOperator m_dev_op{}; + yati::container::Collections m_collections{}; + State m_state{State::None}; +}; + +} // namespace sphaira::ui::menu::gc diff --git a/sphaira/source/ui/menus/gc_menu.cpp b/sphaira/source/ui/menus/gc_menu.cpp new file mode 100644 index 0000000..bd6afb6 --- /dev/null +++ b/sphaira/source/ui/menus/gc_menu.cpp @@ -0,0 +1,187 @@ +#include "ui/menus/gc_menu.hpp" +#include "yati/yati.hpp" +#include "app.hpp" +#include "defines.hpp" +#include "log.hpp" +#include "ui/nvg_util.hpp" +#include "i18n.hpp" +#include + +namespace sphaira::ui::menu::gc { +namespace { + +auto InRange(u64 off, u64 offset, u64 size) -> bool { + return off < offset + size && off >= offset; +} + +struct GcSource final : yati::source::Base { + GcSource(const yati::container::Collections& collections, fs::FsNativeGameCard* fs); + ~GcSource(); + Result Read(void* buf, s64 off, s64 size, u64* bytes_read); + + const yati::container::Collections& m_collections; + fs::FsNativeGameCard* m_fs{}; + FsFile m_file{}; + s64 m_offset{}; + s64 m_size{}; +}; + +GcSource::GcSource(const yati::container::Collections& collections, fs::FsNativeGameCard* fs) +: m_collections{collections} +, m_fs{fs} { + m_offset = -1; +} + +GcSource::~GcSource() { + fsFileClose(&m_file); +} + +Result GcSource::Read(void* buf, s64 off, s64 size, u64* bytes_read) { + // check is we need to open a new file. + if (!InRange(off, m_offset, m_size)) { + fsFileClose(&m_file); + m_file = {}; + + // find new file based on the offset. + bool found = false; + for (auto& collection : m_collections) { + if (InRange(off, collection.offset, collection.size)) { + found = true; + m_offset = collection.offset; + m_size = collection.size; + R_TRY(m_fs->OpenFile(fs::AppendPath("/", collection.name), FsOpenMode_Read, &m_file)); + break; + } + } + + // this will never fail, unless i break something in yati. + R_UNLESS(found, 0x1); + } + + return fsFileRead(&m_file, off - m_offset, buf, size, 0, bytes_read); +} + +} // namespace + +Menu::Menu() : MenuBase{"GameCard"_i18n} { + SetAction(Button::B, Action{"Back"_i18n, [this](){ + SetPop(); + }}); + + SetAction(Button::X, Action{"Refresh"_i18n, [this](){ + m_state = State::None; + }}); + + fsOpenDeviceOperator(std::addressof(m_dev_op)); +} + +Menu::~Menu() { + // manually close this as it needs(?) to be closed before dev_op. + m_fs.reset(); + fsDeviceOperatorClose(std::addressof(m_dev_op)); +} + +void Menu::Update(Controller* controller, TouchInfo* touch) { + MenuBase::Update(controller, touch); + + switch (m_state) { + case State::None: { + bool gc_inserted; + if (R_FAILED(fsDeviceOperatorIsGameCardInserted(std::addressof(m_dev_op), std::addressof(gc_inserted)))) { + m_state = State::Failed; + } else { + if (!gc_inserted) { + m_state = State::NotFound; + } else { + if (R_FAILED(ScanGamecard())) { + m_state = State::Failed; + } + } + } + } break; + + case State::Progress: + case State::Done: + case State::NotFound: + case State::Failed: + break; + } +} + +void Menu::Draw(NVGcontext* vg, Theme* theme) { + MenuBase::Draw(vg, theme); + + switch (m_state) { + case State::None: + gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Waiting for connection..."_i18n.c_str()); + break; + + case State::Progress: + gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Transferring data..."_i18n.c_str()); + break; + + case State::NotFound: + gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "No GameCard inserted, press X to refresh"_i18n.c_str()); + break; + + case State::Done: + gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Installed GameCard, press B to exit..."_i18n.c_str()); + break; + + case State::Failed: + gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Failed to scan GameCard..."_i18n.c_str()); + break; + } +} + +Result Menu::ScanGamecard() { + m_state = State::None; + m_fs.reset(); + m_collections.clear(); + + FsGameCardHandle gc_handle; + R_TRY(fsDeviceOperatorGetGameCardHandle(std::addressof(m_dev_op), std::addressof(gc_handle))); + + m_fs = std::make_unique(std::addressof(gc_handle), FsGameCardPartition_Secure, false); + R_TRY(m_fs->GetFsOpenResult()); + + FsDir dir; + R_TRY(m_fs->OpenDirectory("/", FsDirOpenMode_ReadFiles, std::addressof(dir))); + ON_SCOPE_EXIT(fsDirClose(std::addressof(dir))); + + s64 count; + R_TRY(m_fs->DirGetEntryCount(std::addressof(dir), std::addressof(count))); + + std::vector buf(count); + s64 total_entries; + R_TRY(m_fs->DirRead(std::addressof(dir), std::addressof(total_entries), buf.size(), buf.data())); + m_collections.reserve(total_entries); + + s64 offset{}; + for (s64 i = 0; i < total_entries; i++) { + yati::container::CollectionEntry entry{}; + entry.name = buf[i].name; + entry.offset = offset; + entry.size = buf[i].file_size; + m_collections.emplace_back(entry); + offset += buf[i].file_size; + } + + m_state = State::Progress; + App::Push(std::make_shared("Installing App"_i18n, [this](auto pbox) mutable -> bool { + auto source = std::make_shared(m_collections, m_fs.get()); + return R_SUCCEEDED(yati::InstallFromCollections(pbox, source, m_collections)); + }, [this](bool result){ + if (result) { + App::Notify("Gc install success!"_i18n); + m_state = State::Done; + } else { + App::Notify("Gc install failed!"_i18n); + m_state = State::Failed; + } + })); + + R_SUCCEED(); +} + +} // namespace sphaira::ui::menu::gc diff --git a/sphaira/source/ui/menus/main_menu.cpp b/sphaira/source/ui/menus/main_menu.cpp index 6c2ef8b..70f674c 100644 --- a/sphaira/source/ui/menus/main_menu.cpp +++ b/sphaira/source/ui/menus/main_menu.cpp @@ -3,6 +3,7 @@ #include "ui/menus/themezer.hpp" #include "ui/menus/ghdl.hpp" #include "ui/menus/usb_menu.hpp" +#include "ui/menus/gc_menu.hpp" #include "ui/sidebar.hpp" #include "ui/popup_list.hpp" @@ -323,6 +324,10 @@ MainMenu::MainMenu() { options->Add(std::make_shared("Usb Install"_i18n, [](){ App::Push(std::make_shared()); })); + + options->Add(std::make_shared("GameCard Install"_i18n, [](){ + App::Push(std::make_shared()); + })); } }));