Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ffaa56bc3 | ||
|
|
eca3358e57 | ||
|
|
757e380e08 | ||
|
|
6c1b5de932 | ||
|
|
d79ac126f7 | ||
|
|
2d7763444e | ||
|
|
1dafa2748c | ||
|
|
9f7bf9581c | ||
|
|
8f39acbaa2 | ||
|
|
81469d0ac9 | ||
|
|
1eae35f072 | ||
|
|
5b82e07b1c | ||
|
|
73886c28ae | ||
|
|
eea09f6e57 |
34
README.md
34
README.md
@@ -49,6 +49,40 @@ The `path` field is optional. If left out, it will use the name of the ini to fi
|
||||
|
||||
See `assets/romfs/assoc/` for more examples of file assoc entries.
|
||||
|
||||
## Installing (applications)
|
||||
|
||||
Sphaira can install applications (nsp, xci, nsz, xcz) from various sources (sd card, gamecard, ftp, usb).
|
||||
|
||||
For informantion about the install options, [see the wiki](https://github.com/ITotalJustice/sphaira/wiki/Install).
|
||||
|
||||
### Usb (install)
|
||||
|
||||
The USB protocol is the same as tinfoil, so tools such as [ns-usbloader](https://github.com/developersu/ns-usbloader) and [fluffy](https://github.com/fourminute/Fluffy) should work with sphaira. You may also use the provided python script found [here](tools/usb_install_pc.py).
|
||||
|
||||
### Ftp (install)
|
||||
|
||||
Once you have connected your ftp client to your switch, you can upload files to install into the `install` folder.
|
||||
|
||||
## Building from source
|
||||
|
||||
You will first need to install [devkitPro](https://devkitpro.org/wiki/Getting_Started).
|
||||
|
||||
Next you will need to install the dependencies:
|
||||
```sh
|
||||
sudo pacman -S switch-dev deko3d switch-cmake switch-curl switch-glm switch-zlib
|
||||
```
|
||||
|
||||
Once devkitPro and all dependencies are installed, you can now build sphaira.
|
||||
|
||||
```sh
|
||||
git clone https://github.com/ITotalJustice/sphaira.git
|
||||
cd sphaira
|
||||
cmake --preset MinSizeRel
|
||||
cmake --build --preset MinSizeRel
|
||||
```
|
||||
|
||||
The output will be found in `build/MinSizeRel/sphaira.nro`
|
||||
|
||||
## Credits
|
||||
|
||||
- borealis
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
cmake_minimum_required(VERSION 3.13)
|
||||
|
||||
set(sphaira_VERSION 0.8.0)
|
||||
set(sphaira_VERSION 0.8.1)
|
||||
|
||||
project(sphaira
|
||||
VERSION ${sphaira_VERSION}
|
||||
@@ -41,7 +41,6 @@ add_executable(sphaira
|
||||
source/ui/menus/file_viewer.cpp
|
||||
source/ui/menus/filebrowser.cpp
|
||||
source/ui/menus/homebrew.cpp
|
||||
source/ui/menus/irs_menu.cpp
|
||||
source/ui/menus/main_menu.cpp
|
||||
source/ui/menus/menu_base.cpp
|
||||
source/ui/menus/themezer.cpp
|
||||
@@ -74,7 +73,6 @@ add_executable(sphaira
|
||||
source/nxlink.cpp
|
||||
source/owo.cpp
|
||||
source/swkbd.cpp
|
||||
source/web.cpp
|
||||
source/i18n.cpp
|
||||
source/ftpsrv_helper.cpp
|
||||
|
||||
|
||||
@@ -189,6 +189,7 @@ public:
|
||||
option::OptionBool m_install{INI_SECTION, "install", false};
|
||||
option::OptionBool m_install_sd{INI_SECTION, "install_sd", true};
|
||||
option::OptionLong m_install_prompt{INI_SECTION, "install_prompt", true};
|
||||
option::OptionLong m_boost_mode{INI_SECTION, "boost_mode", false};
|
||||
option::OptionBool m_allow_downgrade{INI_SECTION, "allow_downgrade", false};
|
||||
option::OptionBool m_skip_if_already_installed{INI_SECTION, "skip_if_already_installed", true};
|
||||
option::OptionBool m_ticket_only{INI_SECTION, "ticket_only", false};
|
||||
|
||||
@@ -8,15 +8,14 @@ namespace sphaira::ui {
|
||||
class ErrorBox final : public Widget {
|
||||
public:
|
||||
ErrorBox(Result code, const std::string& message);
|
||||
ErrorBox(const std::string& message);
|
||||
|
||||
auto Update(Controller* controller, TouchInfo* touch) -> void override;
|
||||
auto Draw(NVGcontext* vg, Theme* theme) -> void override;
|
||||
|
||||
private:
|
||||
Result m_code{};
|
||||
std::optional<Result> m_code{};
|
||||
std::string m_message{};
|
||||
std::string m_module_str{};
|
||||
std::string m_description_str{};
|
||||
};
|
||||
|
||||
} // namespace sphaira::ui
|
||||
|
||||
@@ -10,14 +10,16 @@
|
||||
namespace sphaira::ui::menu::gc {
|
||||
|
||||
struct GcCollection : yati::container::CollectionEntry {
|
||||
GcCollection(const char* _name, s64 _size, u8 _type) {
|
||||
GcCollection(const char* _name, s64 _size, u8 _type, u8 _id_offset) {
|
||||
name = _name;
|
||||
size = _size;
|
||||
type = _type;
|
||||
id_offset = _id_offset;
|
||||
}
|
||||
|
||||
// NcmContentType
|
||||
u8 type{};
|
||||
u8 id_offset{};
|
||||
};
|
||||
|
||||
using GcCollections = std::vector<GcCollection>;
|
||||
@@ -48,6 +50,7 @@ private:
|
||||
Result GcMount();
|
||||
void GcUnmount();
|
||||
Result GcPoll(bool* inserted);
|
||||
Result GcOnEvent();
|
||||
Result UpdateStorageSize();
|
||||
|
||||
void FreeImage();
|
||||
@@ -57,6 +60,8 @@ private:
|
||||
FsDeviceOperator m_dev_op{};
|
||||
FsGameCardHandle m_handle{};
|
||||
std::unique_ptr<fs::FsNativeGameCard> m_fs{};
|
||||
FsEventNotifier m_event_notifier{};
|
||||
Event m_event{};
|
||||
|
||||
std::vector<ApplicationEntry> m_entries{};
|
||||
std::unique_ptr<List> m_list{};
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "ui/menus/menu_base.hpp"
|
||||
#include <span>
|
||||
|
||||
namespace sphaira::ui::menu::irs {
|
||||
|
||||
enum Rotation {
|
||||
Rotation_0,
|
||||
Rotation_90,
|
||||
Rotation_180,
|
||||
Rotation_270,
|
||||
};
|
||||
|
||||
enum Colour {
|
||||
Colour_Grey,
|
||||
Colour_Ironbow,
|
||||
Colour_Green,
|
||||
Colour_Red,
|
||||
Colour_Blue,
|
||||
};
|
||||
|
||||
struct Entry {
|
||||
IrsIrCameraHandle m_handle{};
|
||||
IrsIrCameraStatus status{};
|
||||
bool m_update_needed{};
|
||||
};
|
||||
|
||||
struct Menu final : MenuBase {
|
||||
Menu();
|
||||
~Menu();
|
||||
|
||||
void Update(Controller* controller, TouchInfo* touch) override;
|
||||
void Draw(NVGcontext* vg, Theme* theme) override;
|
||||
void OnFocusGained() override;
|
||||
|
||||
void PollCameraStatus(bool statup = false);
|
||||
void LoadDefaultConfig();
|
||||
void UpdateConfig(const IrsImageTransferProcessorExConfig* config);
|
||||
void ResetImage();
|
||||
void UpdateImage();
|
||||
void updateColourArray();
|
||||
|
||||
private:
|
||||
Result m_init_rc{};
|
||||
|
||||
IrsImageTransferProcessorExConfig m_config{};
|
||||
IrsMomentProcessorConfig m_moment_config{};
|
||||
IrsClusteringProcessorConfig m_clustering_config{};
|
||||
IrsTeraPluginProcessorConfig m_tera_config{};
|
||||
IrsIrLedProcessorConfig m_led_config{};
|
||||
IrsAdaptiveClusteringProcessorConfig m_adaptive_config{};
|
||||
IrsHandAnalysisConfig m_hand_config{};
|
||||
|
||||
Entry m_entries[IRS_MAX_CAMERAS]{};
|
||||
u32 m_irs_width{};
|
||||
u32 m_irs_height{};
|
||||
std::vector<u32> m_rgba{};
|
||||
std::vector<u8> m_irs_buffer{};
|
||||
IrsImageTransferProcessorState m_prev_state{};
|
||||
Rotation m_rotation{Rotation_90};
|
||||
Colour m_colour{Colour_Grey};
|
||||
int m_image{};
|
||||
s64 m_index{};
|
||||
};
|
||||
|
||||
} // namespace sphaira::ui::menu::irs
|
||||
@@ -8,8 +8,10 @@ namespace sphaira::ui::menu::usb {
|
||||
enum class State {
|
||||
// not connected.
|
||||
None,
|
||||
// just connected, waiting for file list.
|
||||
Connected_WaitForFileList,
|
||||
// just connected, starts the transfer.
|
||||
Connected,
|
||||
Connected_StartingTransfer,
|
||||
// set whilst transfer is in progress.
|
||||
Progress,
|
||||
// set when the transfer is finished.
|
||||
@@ -35,9 +37,8 @@ struct Menu final : MenuBase {
|
||||
Mutex m_mutex{};
|
||||
// the below are shared across threads, lock with the above mutex!
|
||||
State m_state{State::None};
|
||||
std::vector<std::string> m_names{};
|
||||
bool m_usb_has_connection{};
|
||||
u32 m_usb_speed{};
|
||||
u32 m_usb_count{};
|
||||
};
|
||||
|
||||
} // namespace sphaira::ui::menu::usb
|
||||
|
||||
@@ -226,7 +226,6 @@ struct ThemeMeta {
|
||||
|
||||
struct Theme {
|
||||
ThemeMeta meta;
|
||||
PLSR_BFSTM music;
|
||||
ElementEntry elements[ThemeEntryID_MAX];
|
||||
|
||||
auto GetColour(ThemeEntryID id) const {
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <switch.h>
|
||||
#include <string>
|
||||
|
||||
namespace sphaira {
|
||||
|
||||
// if show_error = true, it will display popup error box on
|
||||
// faliure. set this to false if you want to handle errors
|
||||
// from the caller.
|
||||
auto WebShow(const std::string& url, bool show_error = true) -> Result;
|
||||
|
||||
} // namespace sphaira
|
||||
@@ -175,7 +175,7 @@ struct Header {
|
||||
u8 old_key_gen; // see KeyGenerationOld.
|
||||
u8 kaek_index; // see KeyAreaEncryptionKeyIndex.
|
||||
u64 size;
|
||||
u64 title_id;
|
||||
u64 program_id;
|
||||
u32 context_id;
|
||||
u32 sdk_version;
|
||||
u8 key_gen; // see KeyGeneration.
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
#include "base.hpp"
|
||||
#include "fs.hpp"
|
||||
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <new>
|
||||
#include <switch.h>
|
||||
|
||||
namespace sphaira::yati::source {
|
||||
@@ -21,11 +25,39 @@ struct Usb final : Base {
|
||||
~Usb();
|
||||
|
||||
Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override;
|
||||
Result Finished() const;
|
||||
Result Finished();
|
||||
|
||||
Result Init();
|
||||
Result WaitForConnection(u64 timeout, u32& speed, u32& count);
|
||||
Result GetFileInfo(std::string& name_out, u64& size_out);
|
||||
Result IsUsbConnected(u64 timeout) const;
|
||||
Result WaitForConnection(u64 timeout, std::vector<std::string>& out_names);
|
||||
void SetFileNameForTranfser(const std::string& name);
|
||||
|
||||
public:
|
||||
// custom allocator for std::vector that respects alignment.
|
||||
// https://en.cppreference.com/w/cpp/named_req/Allocator
|
||||
template <typename T, std::size_t Align>
|
||||
struct CustomVectorAllocator {
|
||||
public:
|
||||
// https://en.cppreference.com/w/cpp/memory/new/operator_new
|
||||
auto allocate(std::size_t n) -> T* {
|
||||
return new(align) T[n];
|
||||
}
|
||||
|
||||
// https://en.cppreference.com/w/cpp/memory/new/operator_delete
|
||||
auto deallocate(T* p, std::size_t n) noexcept -> void {
|
||||
::operator delete[] (p, n, align);
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr inline std::align_val_t align{Align};
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
struct PageAllocator : CustomVectorAllocator<T, 0x1000> {
|
||||
using value_type = T; // used by std::vector
|
||||
};
|
||||
|
||||
using PageAlignedVector = std::vector<u8, PageAllocator<u8>>;
|
||||
|
||||
private:
|
||||
enum UsbSessionEndpoint {
|
||||
@@ -33,20 +65,24 @@ private:
|
||||
UsbSessionEndpoint_Out = 1,
|
||||
};
|
||||
|
||||
Result SendCommand(s64 off, s64 size) const;
|
||||
Result InternalRead(void* buf, s64 off, s64 size) const;
|
||||
Result SendCmdHeader(u32 cmdId, size_t dataSize);
|
||||
Result SendFileRangeCmd(u64 offset, u64 size);
|
||||
|
||||
bool GetConfigured() const;
|
||||
Event *GetCompletionEvent(UsbSessionEndpoint ep) const;
|
||||
Result WaitTransferCompletion(UsbSessionEndpoint ep, u64 timeout) const;
|
||||
Result TransferAsync(UsbSessionEndpoint ep, void *buffer, u32 size, u32 *out_urb_id) const;
|
||||
Result GetTransferResult(UsbSessionEndpoint ep, u32 urb_id, u32 *out_requested_size, u32 *out_transferred_size) const;
|
||||
Result TransferPacketImpl(bool read, void *page, u32 size, u32 *out_size_transferred, u64 timeout) const;
|
||||
Result TransferAll(bool read, void *data, u32 size, u64 timeout);
|
||||
|
||||
private:
|
||||
UsbDsInterface* m_interface{};
|
||||
UsbDsEndpoint* m_endpoints[2]{};
|
||||
u64 m_transfer_timeout{};
|
||||
// aligned buffer that transfer data is copied to and from.
|
||||
// a vector is used to avoid multiple alloc within the transfer loop.
|
||||
PageAlignedVector m_aligned{};
|
||||
std::string m_transfer_file_name{};
|
||||
};
|
||||
|
||||
} // namespace sphaira::yati::source
|
||||
|
||||
@@ -71,6 +71,10 @@ enum : Result {
|
||||
struct Config {
|
||||
bool sd_card_install{};
|
||||
|
||||
// sets the performance mode to FastLoad which boosts the CPU clock
|
||||
// and lowers the GPU clock.
|
||||
bool boost_mode{};
|
||||
|
||||
// enables downgrading patch / data patch (dlc) version.
|
||||
bool allow_downgrade{};
|
||||
|
||||
@@ -132,7 +136,7 @@ Result InstallFromSource(ui::ProgressBox* pbox, std::shared_ptr<source::Base> so
|
||||
Result InstallFromContainer(ui::ProgressBox* pbox, std::shared_ptr<container::Base> container, const ConfigOverride& override = {});
|
||||
Result InstallFromCollections(ui::ProgressBox* pbox, std::shared_ptr<source::Base> source, const container::Collections& collections, const ConfigOverride& override = {});
|
||||
|
||||
Result ParseCnmtNca(const fs::FsPath& path, ncm::PackagedContentMeta& header, std::vector<u8>& extended_header, std::vector<NcmPackagedContentInfo>& infos);
|
||||
Result ParseControlNca(const fs::FsPath& path, u64 id, void* nacp_out = nullptr, s64 nacp_size = 0, std::vector<u8>* icon_out = nullptr);
|
||||
Result ParseCnmtNca(const fs::FsPath& path, u64 program_id, ncm::PackagedContentMeta& header, std::vector<u8>& extended_header, std::vector<NcmPackagedContentInfo>& infos);
|
||||
Result ParseControlNca(const fs::FsPath& path, u64 program_id, void* nacp_out = nullptr, s64 nacp_size = 0, std::vector<u8>* icon_out = nullptr);
|
||||
|
||||
} // namespace sphaira::yati
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
#include "ui/error_box.hpp"
|
||||
|
||||
#include "ui/menus/main_menu.hpp"
|
||||
#include "ui/menus/irs_menu.hpp"
|
||||
#include "ui/menus/themezer.hpp"
|
||||
#include "ui/menus/ghdl.hpp"
|
||||
#include "ui/menus/usb_menu.hpp"
|
||||
@@ -26,7 +25,6 @@
|
||||
#include "defines.hpp"
|
||||
#include "i18n.hpp"
|
||||
#include "ftpsrv_helper.hpp"
|
||||
#include "web.hpp"
|
||||
|
||||
#include <nanovg_dk.h>
|
||||
#include <minIni.h>
|
||||
@@ -46,8 +44,33 @@ extern "C" {
|
||||
namespace sphaira {
|
||||
namespace {
|
||||
|
||||
constexpr fs::FsPath DEFAULT_MUSIC_PATH = "/config/sphaira/themes/default_music.bfstm";
|
||||
constexpr const char* DEFAULT_MUSIC_URL = "https://files.catbox.moe/1ovji1.bfstm";
|
||||
// constexpr const char* DEFAULT_MUSIC_URL = "https://raw.githubusercontent.com/ITotalJustice/sphaira/refs/heads/master/assets/default_music.bfstm";
|
||||
|
||||
void download_default_music() {
|
||||
App::Push(std::make_shared<ui::ProgressBox>(0, "Downloading "_i18n, "default_music.bfstm", [](auto pbox){
|
||||
const auto result = curl::Api().ToFile(
|
||||
curl::Url{DEFAULT_MUSIC_URL},
|
||||
curl::Path{DEFAULT_MUSIC_PATH},
|
||||
curl::OnProgress{pbox->OnDownloadProgressCallback()}
|
||||
);
|
||||
|
||||
return result.success;
|
||||
}, [](bool success){
|
||||
if (success) {
|
||||
App::Notify("Downloaded "_i18n + "default_music.bfstm");
|
||||
App::SetTheme(App::GetThemeIndex());
|
||||
} else {
|
||||
App::Push(std::make_shared<ui::ErrorBox>(
|
||||
"Failed to download default_music.bfstm, please try again"_i18n
|
||||
));
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
struct ThemeData {
|
||||
fs::FsPath music_path{"/config/sphaira/themes/default_music.bfstm"};
|
||||
fs::FsPath music_path{DEFAULT_MUSIC_PATH};
|
||||
std::string elements[ThemeEntryID_MAX]{};
|
||||
};
|
||||
|
||||
@@ -1116,7 +1139,6 @@ void App::CloseTheme() {
|
||||
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) {
|
||||
@@ -1146,10 +1168,12 @@ void App::LoadTheme(const ThemeMeta& meta) {
|
||||
|
||||
// load music
|
||||
if (!theme_data.music_path.empty()) {
|
||||
if (R_SUCCEEDED(plsrBFSTMOpen(theme_data.music_path, &m_theme.music))) {
|
||||
if (R_SUCCEEDED(plsrPlayerLoadStream(&m_theme.music, &m_sound_ids[SoundEffect_Music]))) {
|
||||
PLSR_BFSTM music_stream;
|
||||
if (R_SUCCEEDED(plsrBFSTMOpen(theme_data.music_path, &music_stream))) {
|
||||
if (R_SUCCEEDED(plsrPlayerLoadStream(&music_stream, &m_sound_ids[SoundEffect_Music]))) {
|
||||
PlaySoundEffect(SoundEffect_Music);
|
||||
}
|
||||
plsrBFSTMClose(&music_stream);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1312,16 +1336,21 @@ App::App(const char* argv0) {
|
||||
if (R_SUCCEEDED(plsrBFSAROpen("qlaunch:/sound/qlaunch.bfsar", &qlaunch_bfsar))) {
|
||||
ON_SCOPE_EXIT(plsrBFSARClose(&qlaunch_bfsar));
|
||||
|
||||
plsrPlayerLoadSoundByName(&qlaunch_bfsar, "SeGameIconFocus", &m_sound_ids[SoundEffect_Focus]);
|
||||
plsrPlayerLoadSoundByName(&qlaunch_bfsar, "SeGameIconScroll", &m_sound_ids[SoundEffect_Scroll]);
|
||||
plsrPlayerLoadSoundByName(&qlaunch_bfsar, "SeGameIconLimit", &m_sound_ids[SoundEffect_Limit]);
|
||||
plsrPlayerLoadSoundByName(&qlaunch_bfsar, "SeStartupMenu_game", &m_sound_ids[SoundEffect_Startup]);
|
||||
plsrPlayerLoadSoundByName(&qlaunch_bfsar, "SeGameIconAdd", &m_sound_ids[SoundEffect_Install]);
|
||||
plsrPlayerLoadSoundByName(&qlaunch_bfsar, "SeInsertError", &m_sound_ids[SoundEffect_Error]);
|
||||
const auto load_sound = [&](const char* name, u32 id) {
|
||||
if (R_FAILED(plsrPlayerLoadSoundByName(&qlaunch_bfsar, name, &m_sound_ids[id]))) {
|
||||
log_write("[PLSR] failed to load sound effect: %s\n", name);
|
||||
}
|
||||
};
|
||||
|
||||
load_sound("SeGameIconFocus", SoundEffect_Focus);
|
||||
load_sound("SeGameIconScroll", SoundEffect_Scroll);
|
||||
load_sound("SeGameIconLimit", SoundEffect_Limit);
|
||||
load_sound("StartupMenu_Game", SoundEffect_Startup);
|
||||
load_sound("SeGameIconAdd", SoundEffect_Install);
|
||||
load_sound("SeInsertError", 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");
|
||||
@@ -1450,7 +1479,7 @@ void App::DisplayThemeOptions(bool left_side) {
|
||||
auto options = std::make_shared<ui::Sidebar>("Theme Options"_i18n, left_side ? ui::Sidebar::Side::LEFT : ui::Sidebar::Side::RIGHT);
|
||||
ON_SCOPE_EXIT(App::Push(options));
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryArray>("Select Theme"_i18n, theme_items, [theme_items](s64& index_out){
|
||||
options->Add(std::make_shared<ui::SidebarEntryArray>("Select Theme"_i18n, theme_items, [](s64& index_out){
|
||||
App::SetTheme(index_out);
|
||||
}, App::GetThemeIndex()));
|
||||
|
||||
@@ -1461,6 +1490,23 @@ void App::DisplayThemeOptions(bool left_side) {
|
||||
options->Add(std::make_shared<ui::SidebarEntryBool>("12 Hour Time"_i18n, App::Get12HourTimeEnable(), [](bool& enable){
|
||||
App::Set12HourTimeEnable(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryCallback>("Download Default Music"_i18n, [](){
|
||||
// check if we already have music
|
||||
if (fs::FileExists(DEFAULT_MUSIC_PATH)) {
|
||||
App::Push(std::make_shared<ui::OptionBox>(
|
||||
"Overwrite current default music?"_i18n,
|
||||
"No"_i18n, "Yes"_i18n, 0, [](auto op_index){
|
||||
if (op_index && *op_index) {
|
||||
download_default_music();
|
||||
}
|
||||
}
|
||||
));
|
||||
|
||||
} else {
|
||||
download_default_music();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
void App::DisplayNetworkOptions(bool left_side) {
|
||||
@@ -1479,16 +1525,6 @@ void App::DisplayMiscOptions(bool left_side) {
|
||||
App::Push(std::make_shared<ui::menu::gh::Menu>());
|
||||
}));
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryCallback>("Irs"_i18n, [](){
|
||||
App::Push(std::make_shared<ui::menu::irs::Menu>());
|
||||
}));
|
||||
|
||||
if (App::IsApplication()) {
|
||||
options->Add(std::make_shared<ui::SidebarEntryCallback>("Web"_i18n, [](){
|
||||
WebShow("https://lite.duckduckgo.com/lite");
|
||||
}));
|
||||
}
|
||||
|
||||
if (App::GetApp()->m_install.Get()) {
|
||||
if (App::GetFtpEnable()) {
|
||||
options->Add(std::make_shared<ui::SidebarEntryCallback>("Ftp Install"_i18n, [](){
|
||||
@@ -1540,18 +1576,22 @@ void App::DisplayInstallOptions(bool left_side) {
|
||||
install_items.push_back("System memory"_i18n);
|
||||
install_items.push_back("microSD card"_i18n);
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryBool>("Enable"_i18n, App::GetInstallEnable(), [](bool& enable){
|
||||
App::SetInstallEnable(enable);
|
||||
options->Add(std::make_shared<ui::SidebarEntryBool>("Enable"_i18n, App::GetApp()->m_install.Get(), [](bool& enable){
|
||||
App::GetApp()->m_install.Set(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryBool>("Show install warning"_i18n, App::GetInstallPrompt(), [](bool& enable){
|
||||
App::SetInstallPrompt(enable);
|
||||
options->Add(std::make_shared<ui::SidebarEntryBool>("Show install warning"_i18n, App::GetApp()->m_install_prompt.Get(), [](bool& enable){
|
||||
App::GetApp()->m_install_prompt.Set(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryArray>("Install location"_i18n, install_items, [](s64& index_out){
|
||||
App::SetInstallSdEnable(index_out);
|
||||
}, (s64)App::GetInstallSdEnable()));
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryBool>("Boost CPU clock"_i18n, App::GetApp()->m_boost_mode.Get(), [](bool& enable){
|
||||
App::GetApp()->m_boost_mode.Set(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryBool>("Allow downgrade"_i18n, App::GetApp()->m_allow_downgrade.Get(), [](bool& enable){
|
||||
App::GetApp()->m_allow_downgrade.Set(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
@@ -1568,11 +1608,11 @@ void App::DisplayInstallOptions(bool left_side) {
|
||||
App::GetApp()->m_skip_base.Set(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip Patch"_i18n, App::GetApp()->m_skip_patch.Get(), [](bool& enable){
|
||||
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip patch"_i18n, App::GetApp()->m_skip_patch.Get(), [](bool& enable){
|
||||
App::GetApp()->m_skip_patch.Set(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip addon"_i18n, App::GetApp()->m_skip_addon.Get(), [](bool& enable){
|
||||
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip dlc"_i18n, App::GetApp()->m_skip_addon.Get(), [](bool& enable){
|
||||
App::GetApp()->m_skip_addon.Set(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
|
||||
@@ -715,7 +715,7 @@ void write_nca_header_encypted(nca::Header& nca_header, u64 tid, const keys::Key
|
||||
nca_header.magic = NCA3_MAGIC;
|
||||
nca_header.distribution_type = nca::DistributionType_System;
|
||||
nca_header.content_type = type;
|
||||
nca_header.title_id = tid;
|
||||
nca_header.program_id = tid;
|
||||
nca_header.sdk_version = 0x000C1100;
|
||||
nca_header.size = buf.tell();
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -590,6 +590,14 @@ auto InstallApp(ProgressBox* pbox, const Entry& entry) -> bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
// case-insensitive version of str.find()
|
||||
auto FindCaseInsensitive(std::string_view base, std::string_view term) -> bool {
|
||||
const auto it = std::search(base.cbegin(), base.cend(), term.cbegin(), term.cend(), [](char a, char b){
|
||||
return std::toupper(a) == std::toupper(b);
|
||||
});
|
||||
return it != base.cend();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
EntryMenu::EntryMenu(Entry& entry, const LazyImage& default_icon, Menu& menu)
|
||||
@@ -838,12 +846,6 @@ void EntryMenu::SetIndex(s64 index) {
|
||||
}
|
||||
}
|
||||
|
||||
auto toLower(const std::string& str) -> std::string {
|
||||
std::string lower;
|
||||
std::transform(str.cbegin(), str.cend(), std::back_inserter(lower), tolower);
|
||||
return lower;
|
||||
}
|
||||
|
||||
Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"AppStore"_i18n}, m_nro_entries{nro_entries} {
|
||||
fs::FsNativeSd fs;
|
||||
fs.CreateDirectoryRecursively("/switch/sphaira/cache/appstore/icons");
|
||||
@@ -1293,12 +1295,11 @@ void Menu::SetSearch(const std::string& term) {
|
||||
|
||||
m_search_term = term;
|
||||
m_entries_index_search.clear();
|
||||
const auto query = toLower(m_search_term);
|
||||
const auto npos = std::string::npos;
|
||||
const auto query = m_search_term;
|
||||
|
||||
for (u64 i = 0; i < m_entries.size(); i++) {
|
||||
const auto& e = m_entries[i];
|
||||
if (toLower(e.title).find(query) != npos || toLower(e.author).find(query) != npos || toLower(e.details).find(query) != npos || toLower(e.description).find(query) != npos) {
|
||||
if (FindCaseInsensitive(e.title, query) || FindCaseInsensitive(e.author, query) || FindCaseInsensitive(e.description, query)) {
|
||||
m_entries_index_search.emplace_back(i);
|
||||
}
|
||||
}
|
||||
@@ -1327,10 +1328,11 @@ void Menu::SetAuthor() {
|
||||
|
||||
m_author_term = m_entries[m_entries_current[m_index]].author;
|
||||
m_entries_index_author.clear();
|
||||
const auto query = m_author_term;
|
||||
|
||||
for (u64 i = 0; i < m_entries.size(); i++) {
|
||||
const auto& e = m_entries[i];
|
||||
if (e.author.find(m_author_term) != std::string::npos) {
|
||||
if (FindCaseInsensitive(e.author, query)) {
|
||||
m_entries_index_author.emplace_back(i);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,13 @@ auto BuildGcPath(const char* name, const FsGameCardHandle* handle, FsGameCardPar
|
||||
return path;
|
||||
}
|
||||
|
||||
Result fsOpenGameCardDetectionEventNotifier(FsEventNotifier* out) {
|
||||
return serviceDispatch(fsGetServiceSession(), 501,
|
||||
.out_num_objects = 1,
|
||||
.out_objects = &out->s
|
||||
);
|
||||
}
|
||||
|
||||
auto InRange(u64 off, u64 offset, u64 size) -> bool {
|
||||
return off < offset + size && off >= offset;
|
||||
}
|
||||
@@ -175,29 +182,24 @@ Menu::Menu() : MenuBase{"GameCard"_i18n} {
|
||||
m_list = std::make_unique<List>(1, 3, m_pos, v, pad);
|
||||
|
||||
fsOpenDeviceOperator(std::addressof(m_dev_op));
|
||||
fsOpenGameCardDetectionEventNotifier(std::addressof(m_event_notifier));
|
||||
fsEventNotifierGetEventHandle(std::addressof(m_event_notifier), std::addressof(m_event), true);
|
||||
GcOnEvent();
|
||||
UpdateStorageSize();
|
||||
}
|
||||
|
||||
Menu::~Menu() {
|
||||
GcUnmount();
|
||||
eventClose(std::addressof(m_event));
|
||||
fsEventNotifierClose(std::addressof(m_event_notifier));
|
||||
fsDeviceOperatorClose(std::addressof(m_dev_op));
|
||||
}
|
||||
|
||||
void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||
// poll for the gamecard first before handling inputs as the gamecard
|
||||
// may have been removed, thus pressing A would fail.
|
||||
bool inserted{};
|
||||
GcPoll(&inserted);
|
||||
if (m_mounted != inserted) {
|
||||
log_write("gc state changed\n");
|
||||
m_mounted = inserted;
|
||||
if (m_mounted) {
|
||||
log_write("trying to mount\n");
|
||||
m_mounted = R_SUCCEEDED(GcMount());
|
||||
} else {
|
||||
log_write("trying to unmount\n");
|
||||
GcUnmount();
|
||||
}
|
||||
if (R_SUCCEEDED(eventWait(std::addressof(m_event), 0))) {
|
||||
GcOnEvent();
|
||||
}
|
||||
|
||||
MenuBase::Update(controller, touch);
|
||||
@@ -312,7 +314,7 @@ Result Menu::GcMount() {
|
||||
std::vector<u8> extended_header;
|
||||
std::vector<NcmPackagedContentInfo> infos;
|
||||
const auto path = BuildGcPath(e.name, &m_handle);
|
||||
R_TRY(yati::ParseCnmtNca(path, header, extended_header, infos));
|
||||
R_TRY(yati::ParseCnmtNca(path, 0, header, extended_header, infos));
|
||||
|
||||
u8 key_gen;
|
||||
FsRightsId rights_id;
|
||||
@@ -321,23 +323,24 @@ Result Menu::GcMount() {
|
||||
// always add tickets, yati will ignore them if not needed.
|
||||
GcCollections collections;
|
||||
// add cnmt file.
|
||||
collections.emplace_back(e.name, e.file_size, NcmContentType_Meta);
|
||||
collections.emplace_back(e.name, e.file_size, NcmContentType_Meta, 0);
|
||||
|
||||
for (const auto& info : infos) {
|
||||
for (const auto& packed_info : infos) {
|
||||
const auto& info = packed_info.info;
|
||||
// these don't exist for gamecards, however i may copy/paste this code
|
||||
// somewhere so i'm future proofing against myself.
|
||||
if (info.info.content_type == NcmContentType_DeltaFragment) {
|
||||
if (info.content_type == NcmContentType_DeltaFragment) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// find the nca file, this will never fail for gamecards, see above comment.
|
||||
const auto str = hexIdToStr(info.info.content_id);
|
||||
const auto str = hexIdToStr(info.content_id);
|
||||
const auto it = std::find_if(buf.cbegin(), buf.cend(), [str](auto& e){
|
||||
return !std::strncmp(str.str, e.name, std::strlen(str.str));
|
||||
});
|
||||
|
||||
R_UNLESS(it != buf.cend(), yati::Result_NcaNotFound);
|
||||
collections.emplace_back(it->name, it->file_size, info.info.content_type);
|
||||
collections.emplace_back(it->name, it->file_size, info.content_type, info.id_offset);
|
||||
}
|
||||
|
||||
const auto app_id = ncm::GetAppId(header);
|
||||
@@ -409,6 +412,7 @@ Result Menu::GcMount() {
|
||||
}
|
||||
|
||||
OnChangeIndex(0);
|
||||
m_mounted = true;
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
@@ -439,6 +443,28 @@ Result Menu::GcPoll(bool* inserted) {
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result Menu::GcOnEvent() {
|
||||
bool inserted{};
|
||||
R_TRY(GcPoll(&inserted));
|
||||
|
||||
if (m_mounted != inserted) {
|
||||
log_write("gc state changed\n");
|
||||
m_mounted = inserted;
|
||||
if (m_mounted) {
|
||||
log_write("trying to mount\n");
|
||||
m_mounted = R_SUCCEEDED(GcMount());
|
||||
if (m_mounted) {
|
||||
App::PlaySoundEffect(SoundEffect::SoundEffect_Startup);
|
||||
}
|
||||
} else {
|
||||
log_write("trying to unmount\n");
|
||||
GcUnmount();
|
||||
}
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result Menu::UpdateStorageSize() {
|
||||
fs::FsNativeContentStorage fs_nand{FsContentStorageId_User};
|
||||
fs::FsNativeContentStorage fs_sd{FsContentStorageId_SdCard};
|
||||
@@ -475,7 +501,13 @@ void Menu::OnChangeIndex(s64 new_index) {
|
||||
NacpStruct nacp;
|
||||
std::vector<u8> icon;
|
||||
const auto path = BuildGcPath(collection.name.c_str(), &m_handle);
|
||||
if (R_SUCCEEDED(yati::ParseControlNca(path, m_entries[m_entry_index].app_id, &nacp, sizeof(nacp), &icon))) {
|
||||
|
||||
u64 program_id = m_entries[m_entry_index].app_id | collection.id_offset;
|
||||
if (hosversionAtLeast(17, 0, 0)) {
|
||||
fsGetProgramId(&program_id, path, FsContentAttributes_All);
|
||||
}
|
||||
|
||||
if (R_SUCCEEDED(yati::ParseControlNca(path, program_id, &nacp, sizeof(nacp), &icon))) {
|
||||
log_write("managed to parse control nca %s\n", path.s);
|
||||
NacpLanguageEntry* lang_entry{};
|
||||
nacpGetLanguageEntry(&nacp, &lang_entry);
|
||||
|
||||
@@ -1,533 +0,0 @@
|
||||
#include "ui/menus/irs_menu.hpp"
|
||||
#include "ui/sidebar.hpp"
|
||||
#include "ui/popup_list.hpp"
|
||||
#include "app.hpp"
|
||||
#include "defines.hpp"
|
||||
#include "log.hpp"
|
||||
#include "ui/nvg_util.hpp"
|
||||
#include "i18n.hpp"
|
||||
#include <cstring>
|
||||
#include <array>
|
||||
|
||||
namespace sphaira::ui::menu::irs {
|
||||
namespace {
|
||||
|
||||
// from trial and error
|
||||
constexpr u32 GAIN_MIN = 1;
|
||||
constexpr u32 GAIN_MAX = 16;
|
||||
|
||||
consteval auto generte_iron_palette_table() {
|
||||
std::array<u32, 256> array{};
|
||||
|
||||
const u32 iron_palette[] = {
|
||||
0xff000014, 0xff000025, 0xff00002a, 0xff000032, 0xff000036, 0xff00003e, 0xff000042, 0xff00004f,
|
||||
0xff010055, 0xff010057, 0xff02005c, 0xff03005e, 0xff040063, 0xff050065, 0xff070069, 0xff0a0070,
|
||||
0xff0b0073, 0xff0d0075, 0xff0d0076, 0xff100078, 0xff120079, 0xff15007c, 0xff17007d, 0xff1c0081,
|
||||
0xff200084, 0xff220085, 0xff260087, 0xff280089, 0xff2c008a, 0xff2e008b, 0xff32008d, 0xff38008f,
|
||||
0xff390090, 0xff3c0092, 0xff3e0093, 0xff410094, 0xff420095, 0xff450096, 0xff470096, 0xff4c0097,
|
||||
0xff4f0097, 0xff510097, 0xff540098, 0xff560098, 0xff5a0099, 0xff5c0099, 0xff5f009a, 0xff64009b,
|
||||
0xff66009b, 0xff6a009b, 0xff6c009c, 0xff6f009c, 0xff70009c, 0xff73009d, 0xff75009d, 0xff7a009d,
|
||||
0xff7e009d, 0xff7f009d, 0xff83009d, 0xff84009d, 0xff87009d, 0xff89009d, 0xff8b009d, 0xff91009c,
|
||||
0xff93009c, 0xff96009b, 0xff98009b, 0xff9b009b, 0xff9c009b, 0xff9f009b, 0xffa0009b, 0xffa4009b,
|
||||
0xffa7009a, 0xffa8009a, 0xffaa0099, 0xffab0099, 0xffae0198, 0xffaf0198, 0xffb00198, 0xffb30196,
|
||||
0xffb40296, 0xffb60295, 0xffb70395, 0xffb90495, 0xffba0495, 0xffbb0593, 0xffbc0593, 0xffbf0692,
|
||||
0xffc00791, 0xffc00791, 0xffc10990, 0xffc20a8f, 0xffc30b8e, 0xffc40c8d, 0xffc60d8b, 0xffc81088,
|
||||
0xffc91187, 0xffca1385, 0xffcb1385, 0xffcc1582, 0xffcd1681, 0xffce187e, 0xffcf187c, 0xffd11b78,
|
||||
0xffd21c75, 0xffd21d74, 0xffd32071, 0xffd4216f, 0xffd5236b, 0xffd52469, 0xffd72665, 0xffd92a60,
|
||||
0xffda2b5e, 0xffdb2e5a, 0xffdb2f57, 0xffdd3051, 0xffdd314e, 0xffde3347, 0xffdf3444, 0xffe0373a,
|
||||
0xffe03933, 0xffe13a30, 0xffe23c2a, 0xffe33d26, 0xffe43f20, 0xffe4411d, 0xffe5431b, 0xffe64616,
|
||||
0xffe74715, 0xffe74913, 0xffe84a12, 0xffe84c0f, 0xffe94d0e, 0xffea4e0c, 0xffea4f0c, 0xffeb520a,
|
||||
0xffec5409, 0xffec5608, 0xffec5808, 0xffed5907, 0xffed5b06, 0xffee5c06, 0xffee5d05, 0xffef6004,
|
||||
0xffef6104, 0xfff06303, 0xfff06403, 0xfff16603, 0xfff16603, 0xfff16803, 0xfff16902, 0xfff16b02,
|
||||
0xfff26d01, 0xfff26e01, 0xfff37001, 0xfff37101, 0xfff47300, 0xfff47400, 0xfff47600, 0xfff47a00,
|
||||
0xfff57b00, 0xfff57e00, 0xfff57f00, 0xfff68100, 0xfff68200, 0xfff78400, 0xfff78500, 0xfff88800,
|
||||
0xfff88900, 0xfff88a00, 0xfff88c00, 0xfff98d00, 0xfff98e00, 0xfff98f00, 0xfff99100, 0xfffa9400,
|
||||
0xfffa9500, 0xfffb9800, 0xfffb9900, 0xfffb9c00, 0xfffc9d00, 0xfffca000, 0xfffca100, 0xfffda400,
|
||||
0xfffda700, 0xfffda800, 0xfffdab00, 0xfffdac00, 0xfffdae00, 0xfffeaf00, 0xfffeb100, 0xfffeb400,
|
||||
0xfffeb500, 0xfffeb800, 0xfffeb900, 0xfffeba00, 0xfffebb00, 0xfffebd00, 0xfffebe00, 0xfffec200,
|
||||
0xfffec400, 0xfffec500, 0xfffec700, 0xfffec800, 0xfffeca01, 0xfffeca01, 0xfffecc02, 0xfffecf04,
|
||||
0xfffecf04, 0xfffed106, 0xfffed308, 0xfffed50a, 0xfffed60a, 0xfffed80c, 0xfffed90d, 0xffffdb10,
|
||||
0xffffdc14, 0xffffdd16, 0xffffde1b, 0xffffdf1e, 0xffffe122, 0xffffe224, 0xffffe328, 0xffffe531,
|
||||
0xffffe635, 0xffffe73c, 0xffffe83f, 0xffffea46, 0xffffeb49, 0xffffec50, 0xffffed54, 0xffffee5f,
|
||||
0xffffef67, 0xfffff06a, 0xfffff172, 0xfffff177, 0xfffff280, 0xfffff285, 0xfffff38e, 0xfffff49a,
|
||||
0xfffff59e, 0xfffff5a6, 0xfffff6aa, 0xfffff7b3, 0xfffff7b6, 0xfffff8bd, 0xfffff8c1, 0xfffff9ca,
|
||||
0xfffffad1, 0xfffffad4, 0xfffffcdb, 0xfffffcdf, 0xfffffde5, 0xfffffde8, 0xfffffeee, 0xfffffff6
|
||||
};
|
||||
|
||||
for (u32 i = 0; i < 256; i++) {
|
||||
const auto c = iron_palette[i];
|
||||
array[i] = RGBA8_MAXALPHA((c >> 16) & 0xFF, (c >> 8) & 0xFF, (c >> 0) & 0xFF);
|
||||
}
|
||||
|
||||
return array;
|
||||
}
|
||||
|
||||
// ARGB Ironbow palette
|
||||
constexpr auto iron_palette = generte_iron_palette_table();
|
||||
|
||||
void irsConvertConfigExToNormal(const IrsImageTransferProcessorExConfig* ex, IrsImageTransferProcessorConfig* nor) {
|
||||
std::memcpy(nor, ex, sizeof(*nor));
|
||||
}
|
||||
|
||||
void irsConvertConfigNormalToEx(const IrsImageTransferProcessorConfig* nor, IrsImageTransferProcessorExConfig* ex) {
|
||||
std::memcpy(ex, nor, sizeof(*nor));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Menu::Menu() : MenuBase{"Irs"_i18n} {
|
||||
SetAction(Button::B, Action{"Back"_i18n, [this](){
|
||||
SetPop();
|
||||
}});
|
||||
|
||||
SetAction(Button::X, Action{"Options"_i18n, [this](){
|
||||
auto options = std::make_shared<Sidebar>("Options"_i18n, Sidebar::Side::RIGHT);
|
||||
ON_SCOPE_EXIT(App::Push(options));
|
||||
|
||||
SidebarEntryArray::Items controller_str;
|
||||
for (u32 i = 0; i < IRS_MAX_CAMERAS; i++) {
|
||||
const auto& e = m_entries[i];
|
||||
std::string text = "Pad "_i18n + (i == 8 ? "HandHeld"_i18n : std::to_string(i));
|
||||
switch (e.status) {
|
||||
case IrsIrCameraStatus_Available:
|
||||
text += " (Available)"_i18n;
|
||||
break;
|
||||
case IrsIrCameraStatus_Unsupported:
|
||||
text += " (Unsupported)"_i18n;
|
||||
break;
|
||||
case IrsIrCameraStatus_Unconnected:
|
||||
text += " (Unconnected)"_i18n;
|
||||
break;
|
||||
}
|
||||
controller_str.emplace_back(text);
|
||||
}
|
||||
|
||||
SidebarEntryArray::Items rotation_str;
|
||||
rotation_str.emplace_back("0 (Sideways)"_i18n);
|
||||
rotation_str.emplace_back("90 (Flat)"_i18n);
|
||||
rotation_str.emplace_back("180 (-Sideways)"_i18n);
|
||||
rotation_str.emplace_back("270 (Upside down)"_i18n);
|
||||
|
||||
SidebarEntryArray::Items colour_str;
|
||||
colour_str.emplace_back("Grey"_i18n);
|
||||
colour_str.emplace_back("Ironbow"_i18n);
|
||||
colour_str.emplace_back("Green"_i18n);
|
||||
colour_str.emplace_back("Red"_i18n);
|
||||
colour_str.emplace_back("Blue"_i18n);
|
||||
|
||||
SidebarEntryArray::Items light_target_str;
|
||||
light_target_str.emplace_back("All leds"_i18n);
|
||||
light_target_str.emplace_back("Bright group"_i18n);
|
||||
light_target_str.emplace_back("Dim group"_i18n);
|
||||
light_target_str.emplace_back("None"_i18n);
|
||||
|
||||
SidebarEntryArray::Items gain_str;
|
||||
for (u32 i = GAIN_MIN; i <= GAIN_MAX; i++) {
|
||||
gain_str.emplace_back(std::to_string(i));
|
||||
}
|
||||
|
||||
SidebarEntryArray::Items is_negative_image_used_str;
|
||||
is_negative_image_used_str.emplace_back("Normal image"_i18n);
|
||||
is_negative_image_used_str.emplace_back("Negative image"_i18n);
|
||||
|
||||
SidebarEntryArray::Items format_str;
|
||||
format_str.emplace_back("320x240"_i18n);
|
||||
format_str.emplace_back("160x120"_i18n);
|
||||
format_str.emplace_back("80x60"_i18n);
|
||||
if (hosversionAtLeast(4,0,0)) {
|
||||
format_str.emplace_back("40x30"_i18n);
|
||||
format_str.emplace_back("20x15"_i18n);
|
||||
}
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Controller"_i18n, controller_str, [this](s64& index){
|
||||
irsStopImageProcessor(m_entries[m_index].m_handle);
|
||||
m_index = index;
|
||||
UpdateConfig(&m_config);
|
||||
}, m_index));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Rotation"_i18n, rotation_str, [this](s64& index){
|
||||
m_rotation = (Rotation)index;
|
||||
}, m_rotation));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Colour"_i18n, colour_str, [this](s64& index){
|
||||
m_colour = (Colour)index;
|
||||
updateColourArray();
|
||||
}, m_colour));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Light Target"_i18n, light_target_str, [this](s64& index){
|
||||
m_config.light_target = index;
|
||||
UpdateConfig(&m_config);
|
||||
}, m_config.light_target));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Gain"_i18n, gain_str, [this](s64& index){
|
||||
m_config.gain = GAIN_MIN + index;
|
||||
UpdateConfig(&m_config);
|
||||
}, m_config.gain - GAIN_MIN));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Negative Image"_i18n, is_negative_image_used_str, [this](s64& index){
|
||||
m_config.is_negative_image_used = index;
|
||||
UpdateConfig(&m_config);
|
||||
}, m_config.is_negative_image_used));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Format"_i18n, format_str, [this](s64& index){
|
||||
m_config.orig_format = index;
|
||||
m_config.trimming_format = index;
|
||||
UpdateConfig(&m_config);
|
||||
}, m_config.orig_format));
|
||||
|
||||
if (hosversionAtLeast(4,0,0)) {
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Trimming Format"_i18n, format_str, [this](s64& index){
|
||||
// you cannot set trim a larger region than the source
|
||||
if (index < m_config.orig_format) {
|
||||
index = m_config.orig_format;
|
||||
} else {
|
||||
m_config.trimming_format = index;
|
||||
UpdateConfig(&m_config);
|
||||
}
|
||||
}, m_config.orig_format));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryBool>("External Light Filter"_i18n, m_config.is_external_light_filter_enabled, [this](bool& enable){
|
||||
m_config.is_external_light_filter_enabled = enable;
|
||||
UpdateConfig(&m_config);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
}
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Load Default"_i18n, [this](){
|
||||
LoadDefaultConfig();
|
||||
}, true));
|
||||
}});
|
||||
|
||||
if (R_FAILED(m_init_rc = irsInitialize())) {
|
||||
return;
|
||||
}
|
||||
|
||||
static_assert(IRS_MAX_CAMERAS >= 9, "max camaeras has gotten smaller!");
|
||||
|
||||
// open all handles
|
||||
irsGetIrCameraHandle(&m_entries[0].m_handle, HidNpadIdType_No1);
|
||||
irsGetIrCameraHandle(&m_entries[1].m_handle, HidNpadIdType_No2);
|
||||
irsGetIrCameraHandle(&m_entries[2].m_handle, HidNpadIdType_No3);
|
||||
irsGetIrCameraHandle(&m_entries[3].m_handle, HidNpadIdType_No4);
|
||||
irsGetIrCameraHandle(&m_entries[4].m_handle, HidNpadIdType_No5);
|
||||
irsGetIrCameraHandle(&m_entries[5].m_handle, HidNpadIdType_No6);
|
||||
irsGetIrCameraHandle(&m_entries[6].m_handle, HidNpadIdType_No7);
|
||||
irsGetIrCameraHandle(&m_entries[7].m_handle, HidNpadIdType_No8);
|
||||
irsGetIrCameraHandle(&m_entries[8].m_handle, HidNpadIdType_Handheld);
|
||||
// get status of all handles
|
||||
PollCameraStatus(true);
|
||||
// load default config
|
||||
LoadDefaultConfig();
|
||||
}
|
||||
|
||||
Menu::~Menu() {
|
||||
ResetImage();
|
||||
|
||||
for (auto& e : m_entries) {
|
||||
irsStopImageProcessor(e.m_handle);
|
||||
}
|
||||
|
||||
// this closes all handles
|
||||
irsExit();
|
||||
}
|
||||
|
||||
void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||
MenuBase::Update(controller, touch);
|
||||
PollCameraStatus();
|
||||
}
|
||||
|
||||
void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
MenuBase::Draw(vg, theme);
|
||||
|
||||
IrsImageTransferProcessorState state;
|
||||
const auto rc = irsGetImageTransferProcessorState(m_entries[m_index].m_handle, m_irs_buffer.data(), m_irs_buffer.size(), &state);
|
||||
if (R_SUCCEEDED(rc) && state.sampling_number != m_prev_state.sampling_number) {
|
||||
m_prev_state = state;
|
||||
SetSubHeading("Ambient Noise Level: "_i18n + std::to_string(m_prev_state.ambient_noise_level));
|
||||
updateColourArray();
|
||||
}
|
||||
|
||||
if (m_image) {
|
||||
float cx{}, cy{};
|
||||
float w{}, h{};
|
||||
float angle{};
|
||||
|
||||
switch (m_rotation) {
|
||||
case Rotation_0: {
|
||||
const auto scale_x = m_pos.w / float(m_irs_width);
|
||||
const auto scale_y = m_pos.h / float(m_irs_height);
|
||||
const auto scale = std::min(scale_x, scale_y);
|
||||
w = m_irs_width * scale;
|
||||
h = m_irs_height * scale;
|
||||
cx = (m_pos.x + m_pos.w / 2.F) - w / 2.F;
|
||||
cy = (m_pos.y + m_pos.h / 2.F) - h / 2.F;
|
||||
angle = 0;
|
||||
} break;
|
||||
case Rotation_90: {
|
||||
const auto scale_x = m_pos.w / float(m_irs_height);
|
||||
const auto scale_y = m_pos.h / float(m_irs_width);
|
||||
const auto scale = std::min(scale_x, scale_y);
|
||||
w = m_irs_width * scale;
|
||||
h = m_irs_height * scale;
|
||||
cx = (m_pos.x + m_pos.w / 2.F) + h / 2.F;
|
||||
cy = (m_pos.y + m_pos.h / 2.F) - w / 2.F;
|
||||
angle = 90;
|
||||
} break;
|
||||
case Rotation_180: {
|
||||
const auto scale_x = m_pos.w / float(m_irs_width);
|
||||
const auto scale_y = m_pos.h / float(m_irs_height);
|
||||
const auto scale = std::min(scale_x, scale_y);
|
||||
w = m_irs_width * scale;
|
||||
h = m_irs_height * scale;
|
||||
cx = (m_pos.x + m_pos.w / 2.F) + w / 2.F;
|
||||
cy = (m_pos.y + m_pos.h / 2.F) + h / 2.F;
|
||||
angle = 180;
|
||||
} break;
|
||||
case Rotation_270: {
|
||||
const auto scale_x = m_pos.w / float(m_irs_height);
|
||||
const auto scale_y = m_pos.h / float(m_irs_width);
|
||||
const auto scale = std::min(scale_x, scale_y);
|
||||
w = m_irs_width * scale;
|
||||
h = m_irs_height * scale;
|
||||
cx = (m_pos.x + m_pos.w / 2.F) - h / 2.F;
|
||||
cy = (m_pos.y + m_pos.h / 2.F) + w / 2.F;
|
||||
angle = 270;
|
||||
} break;
|
||||
}
|
||||
|
||||
nvgSave(vg);
|
||||
nvgTranslate(vg, cx, cy);
|
||||
const auto paint = nvgImagePattern(vg, 0, 0, w, h, 0, m_image, 1.f);
|
||||
nvgRotate(vg, nvgDegToRad(angle));
|
||||
nvgBeginPath(vg);
|
||||
nvgRect(vg, 0, 0, w, h);
|
||||
nvgFillPaint(vg, paint);
|
||||
nvgFill(vg);
|
||||
nvgRestore(vg);
|
||||
}
|
||||
}
|
||||
|
||||
void Menu::OnFocusGained() {
|
||||
MenuBase::OnFocusGained();
|
||||
}
|
||||
|
||||
void Menu::PollCameraStatus(bool statup) {
|
||||
int index = 0;
|
||||
for (auto& e : m_entries) {
|
||||
IrsIrCameraStatus status;
|
||||
if (R_FAILED(irsGetIrCameraStatus(e.m_handle, &status))) {
|
||||
log_write("failed to get ir status\n");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (e.status != status || statup) {
|
||||
e.status = status;
|
||||
e.m_update_needed = false;
|
||||
|
||||
log_write("status changed\n");
|
||||
switch (e.status) {
|
||||
case IrsIrCameraStatus_Available:
|
||||
if (hosversionAtLeast(4,0,0)) {
|
||||
// calling this breaks the handle, kinda
|
||||
#if 0
|
||||
if (R_FAILED(irsCheckFirmwareUpdateNecessity(e.m_handle, &e.m_update_needed))) {
|
||||
log_write("failed to check if update needed: %u\n", e.m_update_needed);
|
||||
} else {
|
||||
if (e.m_update_needed) {
|
||||
log_write("update needed\n");
|
||||
} else {
|
||||
log_write("no update needed\n");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
log_write("irs index: %d status: IrsIrCameraStatus_Available\n", index);
|
||||
break;
|
||||
case IrsIrCameraStatus_Unsupported:
|
||||
log_write("irs index: %d status: IrsIrCameraStatus_Unsupported\n", index);
|
||||
break;
|
||||
case IrsIrCameraStatus_Unconnected:
|
||||
log_write("irs index: %d status: IrsIrCameraStatus_Unconnected\n", index);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
void Menu::ResetImage() {
|
||||
if (m_image) {
|
||||
nvgDeleteImage(App::GetVg(), m_image);
|
||||
m_image = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void Menu::UpdateImage() {
|
||||
ResetImage();
|
||||
m_image = nvgCreateImageRGBA(App::GetVg(), m_irs_width, m_irs_height, NVG_IMAGE_NEAREST, (const unsigned char*)m_rgba.data());
|
||||
}
|
||||
|
||||
void Menu::LoadDefaultConfig() {
|
||||
IrsImageTransferProcessorExConfig ex_config;
|
||||
|
||||
if (hosversionAtLeast(4,0,0)) {
|
||||
irsGetDefaultImageTransferProcessorExConfig(&ex_config);
|
||||
} else {
|
||||
IrsImageTransferProcessorConfig nor_config;
|
||||
irsGetDefaultImageTransferProcessorConfig(&nor_config);
|
||||
irsConvertConfigNormalToEx(&nor_config, &ex_config);
|
||||
}
|
||||
|
||||
irsGetMomentProcessorDefaultConfig(&m_moment_config);
|
||||
irsGetClusteringProcessorDefaultConfig(&m_clustering_config);
|
||||
irsGetIrLedProcessorDefaultConfig(&m_led_config);
|
||||
|
||||
m_tera_config = {};
|
||||
m_adaptive_config = {};
|
||||
m_hand_config = {};
|
||||
|
||||
UpdateConfig(&ex_config);
|
||||
}
|
||||
|
||||
void Menu::UpdateConfig(const IrsImageTransferProcessorExConfig* config) {
|
||||
m_config = *config;
|
||||
irsStopImageProcessor(m_entries[m_index].m_handle);
|
||||
|
||||
if (R_FAILED(irsRunMomentProcessor(m_entries[m_index].m_handle, &m_moment_config))) {
|
||||
log_write("failed to irsRunMomentProcessor\n");
|
||||
} else {
|
||||
log_write("did irsRunMomentProcessor\n");
|
||||
}
|
||||
|
||||
if (R_FAILED(irsRunClusteringProcessor(m_entries[m_index].m_handle, &m_clustering_config))) {
|
||||
log_write("failed to irsRunClusteringProcessor\n");
|
||||
} else {
|
||||
log_write("did irsRunClusteringProcessor\n");
|
||||
}
|
||||
|
||||
if (R_FAILED(irsRunPointingProcessor(m_entries[m_index].m_handle))) {
|
||||
log_write("failed to irsRunPointingProcessor\n");
|
||||
} else {
|
||||
log_write("did irsRunPointingProcessor\n");
|
||||
}
|
||||
|
||||
if (R_FAILED(irsRunTeraPluginProcessor(m_entries[m_index].m_handle, &m_tera_config))) {
|
||||
log_write("failed to irsRunTeraPluginProcessor\n");
|
||||
} else {
|
||||
log_write("did irsRunTeraPluginProcessor\n");
|
||||
}
|
||||
|
||||
if (R_FAILED(irsRunIrLedProcessor(m_entries[m_index].m_handle, &m_led_config))) {
|
||||
log_write("failed to irsRunIrLedProcessor\n");
|
||||
} else {
|
||||
log_write("did irsRunIrLedProcessor\n");
|
||||
}
|
||||
|
||||
if (R_FAILED(irsRunAdaptiveClusteringProcessor(m_entries[m_index].m_handle, &m_adaptive_config))) {
|
||||
log_write("failed to irsRunAdaptiveClusteringProcessor\n");
|
||||
} else {
|
||||
log_write("did irsRunAdaptiveClusteringProcessor\n");
|
||||
}
|
||||
|
||||
if (R_FAILED(irsRunHandAnalysis(m_entries[m_index].m_handle, &m_hand_config))) {
|
||||
log_write("failed to irsRunHandAnalysis\n");
|
||||
} else {
|
||||
log_write("did irsRunHandAnalysis\n");
|
||||
}
|
||||
|
||||
if (hosversionAtLeast(4,0,0)) {
|
||||
m_init_rc = irsRunImageTransferExProcessor(m_entries[m_index].m_handle, &m_config, 0x10000000);
|
||||
} else {
|
||||
IrsImageTransferProcessorConfig nor;
|
||||
irsConvertConfigExToNormal(&m_config, &nor);
|
||||
m_init_rc = irsRunImageTransferProcessor(m_entries[m_index].m_handle, &nor, 0x10000000);
|
||||
}
|
||||
|
||||
if (R_FAILED(m_init_rc)) {
|
||||
log_write("irs failed to set config!\n");
|
||||
}
|
||||
|
||||
auto format = m_config.orig_format;
|
||||
log_write("IRS CONFIG\n");
|
||||
log_write("\texposure_time: %lu\n", m_config.exposure_time);
|
||||
log_write("\tlight_target: %u\n", m_config.light_target);
|
||||
log_write("\tgain: %u\n", m_config.gain);
|
||||
log_write("\tis_negative_image_used: %u\n", m_config.is_negative_image_used);
|
||||
log_write("\tlight_target: %u\n", m_config.light_target);
|
||||
if (hosversionAtLeast(4,0,0)) {
|
||||
format = m_config.trimming_format;
|
||||
log_write("\ttrimming_format: %u\n", m_config.trimming_format);
|
||||
log_write("\ttrimming_start_x: %u\n", m_config.trimming_start_x);
|
||||
log_write("\ttrimming_start_y: %u\n", m_config.trimming_start_y);
|
||||
log_write("\tis_external_light_filter_enabled: %u\n", m_config.is_external_light_filter_enabled);
|
||||
}
|
||||
|
||||
switch (format) {
|
||||
case IrsImageTransferProcessorFormat_320x240:
|
||||
log_write("\tsetting format: %s\n", "IrsImageTransferProcessorFormat_320x240");
|
||||
m_irs_width = 320;
|
||||
m_irs_height = 240;
|
||||
break;
|
||||
case IrsImageTransferProcessorFormat_160x120:
|
||||
log_write("\tsetting format: %s\n", "IrsImageTransferProcessorFormat_160x120");
|
||||
m_irs_width = 160;
|
||||
m_irs_height = 120;
|
||||
break;
|
||||
case IrsImageTransferProcessorFormat_80x60:
|
||||
log_write("\tsetting format: %s\n", "IrsImageTransferProcessorFormat_80x60");
|
||||
m_irs_width = 80;
|
||||
m_irs_height = 60;
|
||||
break;
|
||||
case IrsImageTransferProcessorFormat_40x30:
|
||||
log_write("\tsetting format: %s\n", "IrsImageTransferProcessorFormat_40x30");
|
||||
m_irs_width = 40;
|
||||
m_irs_height = 30;
|
||||
break;
|
||||
case IrsImageTransferProcessorFormat_20x15:
|
||||
log_write("\tsetting format: %s\n", "IrsImageTransferProcessorFormat_20x15");
|
||||
m_irs_width = 20;
|
||||
m_irs_height = 15;
|
||||
break;
|
||||
}
|
||||
|
||||
m_rgba.resize(m_irs_width * m_irs_height);
|
||||
m_irs_buffer.resize(m_irs_width * m_irs_height);
|
||||
m_prev_state.sampling_number = UINT64_MAX;
|
||||
std::fill(m_irs_buffer.begin(), m_irs_buffer.end(), 0);
|
||||
updateColourArray();
|
||||
}
|
||||
|
||||
void Menu::updateColourArray() {
|
||||
const auto ir_width = m_irs_width;
|
||||
const auto ir_height = m_irs_height;
|
||||
const auto colour = m_colour;
|
||||
|
||||
for (u32 y = 0; y < ir_height; y++) {
|
||||
for (u32 x = 0; x < ir_width; x++) {
|
||||
const u32 pos = y * ir_width + x;
|
||||
const u32 pos2 = y * ir_width + x;
|
||||
|
||||
switch (colour) {
|
||||
case Colour_Grey:
|
||||
m_rgba[pos] = RGBA8_MAXALPHA(m_irs_buffer[pos2], m_irs_buffer[pos2], m_irs_buffer[pos2]);
|
||||
break;
|
||||
case Colour_Ironbow:
|
||||
m_rgba[pos] = iron_palette[m_irs_buffer[pos2]];
|
||||
break;
|
||||
case Colour_Green:
|
||||
m_rgba[pos] = RGBA8_MAXALPHA(0, m_irs_buffer[pos2], 0);
|
||||
break;
|
||||
case Colour_Red:
|
||||
m_rgba[pos] = RGBA8_MAXALPHA(m_irs_buffer[pos2], 0, 0);
|
||||
break;
|
||||
case Colour_Blue:
|
||||
m_rgba[pos] = RGBA8_MAXALPHA(0, 0, m_irs_buffer[pos2]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UpdateImage();
|
||||
}
|
||||
|
||||
} // namespace sphaira::ui::menu::irs
|
||||
@@ -208,6 +208,9 @@ MainMenu::MainMenu() {
|
||||
|
||||
this->SetActions(
|
||||
std::make_pair(Button::START, Action{App::Exit}),
|
||||
std::make_pair(Button::SELECT, Action{"Misc"_i18n, [this](){
|
||||
App::DisplayMiscOptions();
|
||||
}}),
|
||||
std::make_pair(Button::Y, Action{"Menu"_i18n, [this](){
|
||||
auto options = std::make_shared<Sidebar>("Menu Options"_i18n, "v" APP_VERSION_HASH, Sidebar::Side::LEFT);
|
||||
ON_SCOPE_EXIT(App::Push(options));
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
namespace sphaira::ui::menu::usb {
|
||||
namespace {
|
||||
|
||||
constexpr u64 CONNECTION_TIMEOUT = 1e+9 * 3;
|
||||
constexpr u64 TRANSFER_TIMEOUT = 1e+9 * 5;
|
||||
constexpr u64 CONNECTION_TIMEOUT = 1e+9 * 1; // 1 second
|
||||
constexpr u64 TRANSFER_TIMEOUT = 1e+9 * 5; // 5 seconds
|
||||
|
||||
void thread_func(void* user) {
|
||||
auto app = static_cast<Menu*>(user);
|
||||
@@ -21,17 +21,26 @@ void thread_func(void* user) {
|
||||
break;
|
||||
}
|
||||
|
||||
const auto rc = app->m_usb_source->WaitForConnection(CONNECTION_TIMEOUT, app->m_usb_speed, app->m_usb_count);
|
||||
const auto rc = app->m_usb_source->IsUsbConnected(CONNECTION_TIMEOUT);
|
||||
|
||||
// set connected status
|
||||
mutexLock(&app->m_mutex);
|
||||
ON_SCOPE_EXIT(mutexUnlock(&app->m_mutex));
|
||||
if (R_SUCCEEDED(rc)) {
|
||||
app->m_state = State::Connected_WaitForFileList;
|
||||
} else {
|
||||
app->m_state = State::None;
|
||||
}
|
||||
mutexUnlock(&app->m_mutex);
|
||||
|
||||
if (R_SUCCEEDED(rc)) {
|
||||
app->m_state = State::Connected;
|
||||
break;
|
||||
} else if (R_FAILED(rc) && R_VALUE(rc) != 0xEA01) {
|
||||
log_write("got: 0x%X value: 0x%X\n", rc, R_VALUE(rc));
|
||||
app->m_state = State::Failed;
|
||||
break;
|
||||
std::vector<std::string> names;
|
||||
if (R_SUCCEEDED(app->m_usb_source->WaitForConnection(CONNECTION_TIMEOUT, names))) {
|
||||
mutexLock(&app->m_mutex);
|
||||
ON_SCOPE_EXIT(mutexUnlock(&app->m_mutex));
|
||||
app->m_state = State::Connected_StartingTransfer;
|
||||
app->m_names = names;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -95,54 +104,36 @@ void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||
mutexLock(&m_mutex);
|
||||
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
|
||||
|
||||
switch (m_state) {
|
||||
case State::None:
|
||||
break;
|
||||
if (m_state == State::Connected_StartingTransfer) {
|
||||
log_write("set to progress\n");
|
||||
m_state = State::Progress;
|
||||
log_write("got connection\n");
|
||||
App::Push(std::make_shared<ui::ProgressBox>(0, "Installing "_i18n, "", [this](auto pbox) mutable -> bool {
|
||||
ON_SCOPE_EXIT(m_usb_source->Finished());
|
||||
|
||||
case State::Connected:
|
||||
log_write("set to progress\n");
|
||||
m_state = State::Progress;
|
||||
log_write("got connection\n");
|
||||
App::Push(std::make_shared<ui::ProgressBox>(0, "Installing "_i18n, "", [this](auto pbox) mutable -> bool {
|
||||
log_write("inside progress box\n");
|
||||
for (u32 i = 0; i < m_usb_count; i++) {
|
||||
std::string file_name;
|
||||
u64 file_size;
|
||||
if (R_FAILED(m_usb_source->GetFileInfo(file_name, file_size))) {
|
||||
return false;
|
||||
}
|
||||
log_write("inside progress box\n");
|
||||
for (const auto& file_name : m_names) {
|
||||
m_usb_source->SetFileNameForTranfser(file_name);
|
||||
|
||||
log_write("got file name: %s size: %lX\n", file_name.c_str(), file_size);
|
||||
|
||||
const auto rc = yati::InstallFromSource(pbox, m_usb_source, file_name);
|
||||
if (R_FAILED(rc)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
App::Notify("Installed via usb"_i18n);
|
||||
m_usb_source->Finished();
|
||||
const auto rc = yati::InstallFromSource(pbox, m_usb_source, file_name);
|
||||
if (R_FAILED(rc)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}, [this](bool result){
|
||||
if (result) {
|
||||
App::Notify("Usb install success!"_i18n);
|
||||
m_state = State::Done;
|
||||
} else {
|
||||
App::Notify("Usb install failed!"_i18n);
|
||||
m_state = State::Failed;
|
||||
}
|
||||
}));
|
||||
break;
|
||||
App::Notify("Installed via usb"_i18n);
|
||||
}
|
||||
|
||||
case State::Progress:
|
||||
break;
|
||||
|
||||
case State::Done:
|
||||
break;
|
||||
|
||||
case State::Failed:
|
||||
break;
|
||||
return true;
|
||||
}, [this](bool result){
|
||||
if (result) {
|
||||
App::Notify("Usb install success!"_i18n);
|
||||
m_state = State::Done;
|
||||
SetPop();
|
||||
} else {
|
||||
App::Notify("Usb install failed!"_i18n);
|
||||
m_state = State::Failed;
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,7 +148,12 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
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::Connected:
|
||||
case State::Connected_WaitForFileList:
|
||||
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Connected, waiting for file list..."_i18n.c_str());
|
||||
break;
|
||||
|
||||
case State::Connected_StartingTransfer:
|
||||
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Connected, starting transfer..."_i18n.c_str());
|
||||
break;
|
||||
|
||||
case State::Progress:
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
#include "web.hpp"
|
||||
#include "log.hpp"
|
||||
#include "defines.hpp"
|
||||
#include <cstring>
|
||||
|
||||
namespace sphaira {
|
||||
|
||||
auto WebShow(const std::string& url, bool show_error) -> Result {
|
||||
// showError("Running in applet mode\nPlease launch hbmenu by holding R on an APP (e.g. a game) NOT an applet (e.g. Gallery)", "", 0);
|
||||
// showError("Error: Nag active, check more details", "Browser won't launch if supernag is active\n\nUse gagorder or switch-sys-tweak (the latter is bundled with BrowseNX) to disable supernag.", 0);
|
||||
// log_write("web show with url: %s\n", url.c_str());
|
||||
// return 0;
|
||||
WebCommonConfig config{};
|
||||
WebCommonReply reply{};
|
||||
WebExitReason reason{};
|
||||
AccountUid account_uid{};
|
||||
char last_url[FS_MAX_PATH]{};
|
||||
size_t last_url_len{};
|
||||
|
||||
// WebBackgroundKind_Unknown1 = shows background
|
||||
// WebBackgroundKind_Unknown2 = shows background faded
|
||||
if (R_FAILED(accountTrySelectUserWithoutInteraction(&account_uid, false))) { log_write("failed: accountTrySelectUserWithoutInteraction\n"); }
|
||||
if (R_FAILED(webPageCreate(&config, url.c_str()))) { log_write("failed: webPageCreate\n"); }
|
||||
if (R_FAILED(webConfigSetWhitelist(&config, "^http"))) { log_write("failed: webConfigSetWhitelist\n"); }
|
||||
if (R_FAILED(webConfigSetEcClientCert(&config, true))) { log_write("failed: webConfigSetEcClientCert\n"); }
|
||||
if (R_FAILED(webConfigSetScreenShot(&config, true))) { log_write("failed: webConfigSetScreenShot\n"); }
|
||||
if (R_FAILED(webConfigSetBootDisplayKind(&config, WebBootDisplayKind_Black))) { log_write("failed: webConfigSetBootDisplayKind\n"); }
|
||||
if (R_FAILED(webConfigSetBackgroundKind(&config, WebBackgroundKind_Default))) { log_write("failed: webConfigSetBackgroundKind\n"); }
|
||||
if (R_FAILED(webConfigSetPointer(&config, true))) { log_write("failed: webConfigSetPointer\n"); }
|
||||
if (R_FAILED(webConfigSetLeftStickMode(&config, WebLeftStickMode_Pointer))) { log_write("failed: webConfigSetLeftStickMode\n"); }
|
||||
// if (R_FAILED(webConfigSetBootAsMediaPlayer(&config, true))) { log_write("failed: webConfigSetBootAsMediaPlayer\n"); }
|
||||
if (R_FAILED(webConfigSetJsExtension(&config, true))) { log_write("failed: webConfigSetJsExtension\n"); }
|
||||
if (R_FAILED(webConfigSetMediaPlayerAutoClose(&config, true))) { log_write("failed: webConfigSetMediaPlayerAutoClose\n"); }
|
||||
if (R_FAILED(webConfigSetPageCache(&config, true))) { log_write("failed: webConfigSetPageCache\n"); }
|
||||
if (R_FAILED(webConfigSetFooterFixedKind(&config, WebFooterFixedKind_Hidden))) { log_write("failed: webConfigSetFooterFixedKind\n"); }
|
||||
if (R_FAILED(webConfigSetPageFade(&config, true))) { log_write("failed: webConfigSetPageFade\n"); }
|
||||
if (R_FAILED(webConfigSetPageScrollIndicator(&config, true))) { log_write("failed: webConfigSetPageScrollIndicator\n"); }
|
||||
// if (R_FAILED(webConfigSetMediaPlayerSpeedControl(&config, true))) { log_write("failed: webConfigSetMediaPlayerSpeedControl\n"); }
|
||||
if (R_FAILED(webConfigSetBootMode(&config, WebSessionBootMode_AllForeground))) { log_write("failed: webConfigSetBootMode\n"); }
|
||||
if (R_FAILED(webConfigSetTransferMemory(&config, true))) { log_write("failed: webConfigSetTransferMemory\n"); }
|
||||
if (R_FAILED(webConfigSetTouchEnabledOnContents(&config, true))) { log_write("failed: webConfigSetTouchEnabledOnContents\n"); }
|
||||
// if (R_FAILED(webConfigSetMediaPlayerUi(&config, true))) { log_write("failed: webConfigSetMediaPlayerUi\n"); }
|
||||
// if (R_FAILED(webConfigSetWebAudio(&config, true))) { log_write("failed: webConfigSetWebAudio\n"); }
|
||||
if (R_FAILED(webConfigSetPageCache(&config, true))) { log_write("failed: webConfigSetPageCache\n"); }
|
||||
if (R_FAILED(webConfigSetBootLoadingIcon(&config, true))) { log_write("failed: webConfigSetBootLoadingIcon\n"); }
|
||||
if (R_FAILED(webConfigSetUid(&config, account_uid))) { log_write("failed: webConfigSetUid\n"); }
|
||||
if (R_FAILED(webConfigShow(&config, &reply))) { log_write("failed: webConfigShow\n"); }
|
||||
if (R_FAILED(webReplyGetExitReason(&reply, &reason))) { log_write("failed: webReplyGetExitReason\n"); }
|
||||
if (R_FAILED(webReplyGetLastUrl(&reply, last_url, sizeof(last_url), &last_url_len))) { log_write("failed: webReplyGetLastUrl\n"); }
|
||||
log_write("last url: %s\n", last_url);
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
} // namespace sphaira
|
||||
@@ -14,33 +14,58 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// Most of the usb transfer code was taken from Haze.
|
||||
// The USB transfer code was taken from Haze (part of Atmosphere).
|
||||
// The USB protocol was taken from Tinfoil, by Adubbz.
|
||||
|
||||
#include "yati/source/usb.hpp"
|
||||
#include "log.hpp"
|
||||
#include <ranges>
|
||||
|
||||
namespace sphaira::yati::source {
|
||||
namespace {
|
||||
|
||||
constexpr u32 MAGIC = 0x53504841;
|
||||
constexpr u32 VERSION = 2;
|
||||
|
||||
struct SendHeader {
|
||||
u32 magic;
|
||||
u32 version;
|
||||
enum USBCmdType : u8 {
|
||||
REQUEST = 0,
|
||||
RESPONSE = 1
|
||||
};
|
||||
|
||||
struct RecvHeader {
|
||||
u32 magic;
|
||||
u32 version;
|
||||
u32 bcdUSB;
|
||||
u32 count;
|
||||
enum USBCmdId : u32 {
|
||||
EXIT = 0,
|
||||
FILE_RANGE = 1
|
||||
};
|
||||
|
||||
struct NX_PACKED USBCmdHeader {
|
||||
u32 magic;
|
||||
USBCmdType type;
|
||||
u8 padding[0x3] = {0};
|
||||
u32 cmdId;
|
||||
u64 dataSize;
|
||||
u8 reserved[0xC] = {0};
|
||||
};
|
||||
|
||||
struct FileRangeCmdHeader {
|
||||
u64 size;
|
||||
u64 offset;
|
||||
u64 nspNameLen;
|
||||
u64 padding;
|
||||
};
|
||||
|
||||
struct TUSHeader {
|
||||
u32 magic; // TUL0 (Tinfoil Usb List 0)
|
||||
u32 nspListSize;
|
||||
u64 padding;
|
||||
};
|
||||
|
||||
static_assert(sizeof(TUSHeader) == 0x10, "TUSHeader must be 0x10!");
|
||||
static_assert(sizeof(USBCmdHeader) == 0x20, "USBCmdHeader must be 0x20!");
|
||||
|
||||
} // namespace
|
||||
|
||||
Usb::Usb(u64 transfer_timeout) {
|
||||
m_open_result = usbDsInitialize();
|
||||
m_transfer_timeout = transfer_timeout;
|
||||
// this avoids allocations during transfers.
|
||||
m_aligned.reserve(1024 * 1024 * 16);
|
||||
}
|
||||
|
||||
Usb::~Usb() {
|
||||
@@ -53,6 +78,11 @@ Result Usb::Init() {
|
||||
log_write("doing USB init\n");
|
||||
R_TRY(m_open_result);
|
||||
|
||||
SetSysSerialNumber serial_number;
|
||||
R_TRY(setsysInitialize());
|
||||
ON_SCOPE_EXIT(setsysExit());
|
||||
R_TRY(setsysGetSerialNumber(&serial_number));
|
||||
|
||||
u8 iManufacturer, iProduct, iSerialNumber;
|
||||
static const u16 supported_langs[1] = {0x0409};
|
||||
// Send language descriptor
|
||||
@@ -62,7 +92,7 @@ Result Usb::Init() {
|
||||
// Send product
|
||||
R_TRY(usbDsAddUsbStringDescriptor(&iProduct, "Nintendo Switch"));
|
||||
// Send serial number
|
||||
R_TRY(usbDsAddUsbStringDescriptor(&iSerialNumber, "SerialNumber"));
|
||||
R_TRY(usbDsAddUsbStringDescriptor(&iSerialNumber, serial_number.number));
|
||||
|
||||
// Send device descriptors
|
||||
struct usb_device_descriptor device_descriptor = {
|
||||
@@ -131,13 +161,11 @@ Result Usb::Init() {
|
||||
.bInterfaceProtocol = USB_CLASS_VENDOR_SPEC,
|
||||
};
|
||||
|
||||
|
||||
struct usb_endpoint_descriptor endpoint_descriptor_in = {
|
||||
.bLength = USB_DT_ENDPOINT_SIZE,
|
||||
.bDescriptorType = USB_DT_ENDPOINT,
|
||||
.bEndpointAddress = USB_ENDPOINT_IN,
|
||||
.bmAttributes = USB_TRANSFER_TYPE_BULK,
|
||||
.wMaxPacketSize = 0x40,
|
||||
};
|
||||
|
||||
struct usb_endpoint_descriptor endpoint_descriptor_out = {
|
||||
@@ -145,7 +173,6 @@ Result Usb::Init() {
|
||||
.bDescriptorType = USB_DT_ENDPOINT,
|
||||
.bEndpointAddress = USB_ENDPOINT_OUT,
|
||||
.bmAttributes = USB_TRANSFER_TYPE_BULK,
|
||||
.wMaxPacketSize = 0x40,
|
||||
};
|
||||
|
||||
const struct usb_ss_endpoint_companion_descriptor endpoint_companion = {
|
||||
@@ -163,6 +190,8 @@ Result Usb::Init() {
|
||||
endpoint_descriptor_out.bEndpointAddress += interface_descriptor.bInterfaceNumber + 1;
|
||||
|
||||
// Full Speed Config
|
||||
endpoint_descriptor_in.wMaxPacketSize = 0x40;
|
||||
endpoint_descriptor_out.wMaxPacketSize = 0x40;
|
||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Full, &interface_descriptor, USB_DT_INTERFACE_SIZE));
|
||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Full, &endpoint_descriptor_in, USB_DT_ENDPOINT_SIZE));
|
||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Full, &endpoint_descriptor_out, USB_DT_ENDPOINT_SIZE));
|
||||
@@ -194,62 +223,38 @@ Result Usb::Init() {
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result Usb::WaitForConnection(u64 timeout, u32& speed, u32& count) {
|
||||
const SendHeader send_header{
|
||||
.magic = MAGIC,
|
||||
.version = VERSION,
|
||||
};
|
||||
Result Usb::IsUsbConnected(u64 timeout) const {
|
||||
return usbDsWaitReady(timeout);
|
||||
}
|
||||
|
||||
alignas(0x1000) u8 aligned[0x1000]{};
|
||||
std::memcpy(aligned, std::addressof(send_header), sizeof(send_header));
|
||||
Result Usb::WaitForConnection(u64 timeout, std::vector<std::string>& out_names) {
|
||||
TUSHeader header;
|
||||
R_TRY(TransferAll(true, &header, sizeof(header), timeout));
|
||||
R_UNLESS(header.magic == 0x304C5554, Result_BadMagic);
|
||||
R_UNLESS(header.nspListSize > 0, Result_BadCount);
|
||||
log_write("USB got header\n");
|
||||
|
||||
// send header.
|
||||
u32 transferredSize;
|
||||
R_TRY(TransferPacketImpl(false, aligned, sizeof(send_header), &transferredSize, timeout));
|
||||
std::vector<char> names(header.nspListSize);
|
||||
R_TRY(TransferAll(true, names.data(), names.size(), timeout));
|
||||
|
||||
// receive header.
|
||||
struct RecvHeader recv_header{};
|
||||
R_TRY(TransferPacketImpl(true, aligned, sizeof(recv_header), &transferredSize, timeout));
|
||||
out_names.clear();
|
||||
for (const auto& name : std::views::split(names, '\n')) {
|
||||
if (!name.empty()) {
|
||||
out_names.emplace_back(name.data(), name.size());
|
||||
}
|
||||
}
|
||||
|
||||
// copy data into header struct.
|
||||
std::memcpy(&recv_header, aligned, sizeof(recv_header));
|
||||
for (auto& name : out_names) {
|
||||
log_write("got name: %s\n", name.c_str());
|
||||
}
|
||||
|
||||
// validate received header.
|
||||
R_UNLESS(recv_header.magic == MAGIC, Result_BadMagic);
|
||||
R_UNLESS(recv_header.version == VERSION, Result_BadVersion);
|
||||
R_UNLESS(recv_header.count > 0, Result_BadCount);
|
||||
|
||||
count = recv_header.count;
|
||||
speed = recv_header.bcdUSB;
|
||||
R_UNLESS(!out_names.empty(), Result_BadCount);
|
||||
log_write("USB SUCCESS\n");
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result Usb::GetFileInfo(std::string& name_out, u64& size_out) {
|
||||
struct {
|
||||
u64 size;
|
||||
u64 name_length;
|
||||
} file_info_meta;
|
||||
|
||||
alignas(0x1000) u8 aligned[0x1000]{};
|
||||
|
||||
// receive meta.
|
||||
u32 transferredSize;
|
||||
R_TRY(TransferPacketImpl(true, aligned, sizeof(file_info_meta), &transferredSize, m_transfer_timeout));
|
||||
std::memcpy(&file_info_meta, aligned, sizeof(file_info_meta));
|
||||
R_UNLESS(file_info_meta.name_length < sizeof(aligned), 0x1);
|
||||
|
||||
R_TRY(TransferPacketImpl(true, aligned, file_info_meta.name_length, &transferredSize, m_transfer_timeout));
|
||||
name_out.resize(file_info_meta.name_length);
|
||||
std::memcpy(name_out.data(), aligned, name_out.size());
|
||||
|
||||
size_out = file_info_meta.size;
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
bool Usb::GetConfigured() const {
|
||||
UsbState usb_state;
|
||||
usbDsGetState(std::addressof(usb_state));
|
||||
return usb_state == UsbState_Configured;
|
||||
void Usb::SetFileNameForTranfser(const std::string& name) {
|
||||
m_transfer_file_name = name;
|
||||
}
|
||||
|
||||
Event *Usb::GetCompletionEvent(UsbSessionEndpoint ep) const {
|
||||
@@ -286,12 +291,7 @@ Result Usb::TransferPacketImpl(bool read, void *page, u32 size, u32 *out_size_tr
|
||||
u32 urb_id;
|
||||
|
||||
/* If we're not configured yet, wait to become configured first. */
|
||||
// R_TRY(usbDsWaitReady(timeout));
|
||||
if (!GetConfigured()) {
|
||||
R_TRY(eventWait(usbDsGetStateChangeEvent(), timeout));
|
||||
R_TRY(eventClear(usbDsGetStateChangeEvent()));
|
||||
R_THROW(0xEA01);
|
||||
}
|
||||
R_TRY(IsUsbConnected(timeout));
|
||||
|
||||
/* Select the appropriate endpoint and begin a transfer. */
|
||||
const auto ep = read ? UsbSessionEndpoint_Out : UsbSessionEndpoint_In;
|
||||
@@ -304,78 +304,73 @@ Result Usb::TransferPacketImpl(bool read, void *page, u32 size, u32 *out_size_tr
|
||||
return GetTransferResult(ep, urb_id, nullptr, out_size_transferred);
|
||||
}
|
||||
|
||||
Result Usb::SendCommand(s64 off, s64 size) const {
|
||||
struct {
|
||||
u32 hash;
|
||||
u32 magic;
|
||||
s64 off;
|
||||
s64 size;
|
||||
} meta{0, 0, off, size};
|
||||
|
||||
alignas(0x1000) static u8 aligned[0x1000]{};
|
||||
std::memcpy(aligned, std::addressof(meta), sizeof(meta));
|
||||
|
||||
u32 transferredSize;
|
||||
return TransferPacketImpl(false, aligned, sizeof(meta), &transferredSize, m_transfer_timeout);
|
||||
}
|
||||
|
||||
Result Usb::Finished() const {
|
||||
return SendCommand(0, 0);
|
||||
}
|
||||
|
||||
Result Usb::InternalRead(void* _buf, s64 off, s64 size) const {
|
||||
u8* buf = (u8*)_buf;
|
||||
alignas(0x1000) u8 aligned[0x1000]{};
|
||||
const auto stored_size = size;
|
||||
s64 total = 0;
|
||||
// while it may seem like a bad idea to transfer data to a buffer and copy it
|
||||
// in practice, this has no impact on performance.
|
||||
// the switch is *massively* bottlenecked by slow io (nand and sd).
|
||||
// so making usb transfers zero-copy provides no benefit other than increased
|
||||
// code complexity and the increase of future bugs if/when sphaira is forked
|
||||
// an changes are made.
|
||||
// yati already goes to great lengths to be zero-copy during installing
|
||||
// by swapping buffers and inflating in-place.
|
||||
Result Usb::TransferAll(bool read, void *data, u32 size, u64 timeout) {
|
||||
auto buf = static_cast<u8*>(data);
|
||||
m_aligned.resize((size + 0xFFF) & ~0xFFF);
|
||||
|
||||
while (size) {
|
||||
auto read_size = size;
|
||||
auto read_buf = buf;
|
||||
|
||||
if (u64(buf) & 0xFFF) {
|
||||
read_size = std::min<u64>(size, sizeof(aligned) - (u64(buf) & 0xFFF));
|
||||
read_buf = aligned;
|
||||
log_write("unaligned read %zd %zd read_size: %zd align: %zd\n", off, size, read_size, u64(buf) & 0xFFF);
|
||||
} else if (read_size & 0xFFF) {
|
||||
if (read_size <= 0xFFF) {
|
||||
log_write("unaligned small read %zd %zd read_size: %zd align: %zd\n", off, size, read_size, u64(buf) & 0xFFF);
|
||||
read_buf = aligned;
|
||||
} else {
|
||||
log_write("unaligned big read %zd %zd read_size: %zd align: %zd\n", off, size, read_size, u64(buf) & 0xFFF);
|
||||
// read as much as possible into buffer, the rest will
|
||||
// be handled in a second read which will be aligned size aligned.
|
||||
read_size = read_size & ~0xFFF;
|
||||
}
|
||||
if (!read) {
|
||||
std::memcpy(m_aligned.data(), buf, size);
|
||||
}
|
||||
|
||||
R_TRY(SendCommand(off, read_size));
|
||||
u32 out_size_transferred;
|
||||
R_TRY(TransferPacketImpl(read, m_aligned.data(), size, &out_size_transferred, timeout));
|
||||
|
||||
u32 transferredSize{};
|
||||
R_TRY(TransferPacketImpl(true, read_buf, read_size, &transferredSize, m_transfer_timeout));
|
||||
R_UNLESS(transferredSize <= read_size, Result_BadTransferSize);
|
||||
|
||||
if (read_buf == aligned) {
|
||||
std::memcpy(buf, aligned, transferredSize);
|
||||
if (read) {
|
||||
std::memcpy(buf, m_aligned.data(), out_size_transferred);
|
||||
}
|
||||
|
||||
if (transferredSize < read_size) {
|
||||
log_write("reading less than expected! %u vs %zd stored: %zd\n", transferredSize, read_size, stored_size);
|
||||
}
|
||||
|
||||
off += transferredSize;
|
||||
buf += transferredSize;
|
||||
size -= transferredSize;
|
||||
total += transferredSize;
|
||||
buf += out_size_transferred;
|
||||
size -= out_size_transferred;
|
||||
}
|
||||
|
||||
R_UNLESS(total == stored_size, Result_BadTotalSize);
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result Usb::SendCmdHeader(u32 cmdId, size_t dataSize) {
|
||||
USBCmdHeader header{
|
||||
.magic = 0x30435554, // TUC0 (Tinfoil USB Command 0)
|
||||
.type = USBCmdType::REQUEST,
|
||||
.cmdId = cmdId,
|
||||
.dataSize = dataSize,
|
||||
};
|
||||
|
||||
return TransferAll(false, &header, sizeof(header), m_transfer_timeout);
|
||||
}
|
||||
|
||||
Result Usb::SendFileRangeCmd(u64 off, u64 size) {
|
||||
FileRangeCmdHeader fRangeHeader;
|
||||
fRangeHeader.size = size;
|
||||
fRangeHeader.offset = off;
|
||||
fRangeHeader.nspNameLen = m_transfer_file_name.size();
|
||||
fRangeHeader.padding = 0;
|
||||
|
||||
R_TRY(SendCmdHeader(USBCmdId::FILE_RANGE, sizeof(fRangeHeader) + fRangeHeader.nspNameLen));
|
||||
R_TRY(TransferAll(false, &fRangeHeader, sizeof(fRangeHeader), m_transfer_timeout));
|
||||
R_TRY(TransferAll(false, m_transfer_file_name.data(), m_transfer_file_name.size(), m_transfer_timeout));
|
||||
|
||||
USBCmdHeader responseHeader;
|
||||
R_TRY(TransferAll(true, &responseHeader, sizeof(responseHeader), m_transfer_timeout));
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result Usb::Finished() {
|
||||
return SendCmdHeader(USBCmdId::EXIT, 0);
|
||||
}
|
||||
|
||||
Result Usb::Read(void* buf, s64 off, s64 size, u64* bytes_read) {
|
||||
R_TRY(GetOpenResult());
|
||||
R_TRY(InternalRead(buf, off, size));
|
||||
R_TRY(SendFileRangeCmd(off, size));
|
||||
R_TRY(TransferAll(true, buf, size, m_transfer_timeout));
|
||||
*bytes_read = size;
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
#include "i18n.hpp"
|
||||
#include "log.hpp"
|
||||
|
||||
#include <new>
|
||||
#include <zstd.h>
|
||||
#include <minIni.h>
|
||||
|
||||
@@ -30,40 +29,10 @@ constexpr NcmStorageId NCM_STORAGE_IDS[]{
|
||||
NcmStorageId_SdCard,
|
||||
};
|
||||
|
||||
// custom allocator for std::vector that respects alignment.
|
||||
// https://en.cppreference.com/w/cpp/named_req/Allocator
|
||||
template <typename T, std::size_t Align>
|
||||
struct CustomVectorAllocator {
|
||||
public:
|
||||
// https://en.cppreference.com/w/cpp/memory/new/operator_new
|
||||
auto allocate(std::size_t n) -> T* {
|
||||
// log_write("allocating ptr size: %zu\n", n);
|
||||
return new(align) T[n];
|
||||
}
|
||||
|
||||
// https://en.cppreference.com/w/cpp/memory/new/operator_delete
|
||||
auto deallocate(T* p, std::size_t n) noexcept -> void {
|
||||
// log_write("deleting ptr size: %zu\n", n);
|
||||
::operator delete[] (p, n, align);
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr inline std::align_val_t align{Align};
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
struct PageAllocator : CustomVectorAllocator<T, 0x1000> {
|
||||
using value_type = T; // used by std::vector
|
||||
};
|
||||
|
||||
template<class T, class U>
|
||||
bool operator==(const PageAllocator <T>&, const PageAllocator <U>&) { return true; }
|
||||
|
||||
using PageAlignedVector = std::vector<u8, PageAllocator<u8>>;
|
||||
|
||||
constexpr u32 KEYGEN_LIMIT = 0x20;
|
||||
|
||||
struct NcaCollection : container::CollectionEntry {
|
||||
nca::Header header{};
|
||||
// NcmContentType
|
||||
u8 type{};
|
||||
NcmContentId content_id{};
|
||||
@@ -72,6 +41,8 @@ struct NcaCollection : container::CollectionEntry {
|
||||
u8 hash[SHA256_HASH_SIZE]{};
|
||||
// set true if nca has been modified.
|
||||
bool modified{};
|
||||
// set if the nca was not installed.
|
||||
bool skipped{};
|
||||
};
|
||||
|
||||
struct CnmtCollection : NcaCollection {
|
||||
@@ -81,7 +52,7 @@ struct CnmtCollection : NcaCollection {
|
||||
// if set, the ticket / cert will be installed once all nca's have installed.
|
||||
std::vector<FsRightsId> rights_id{};
|
||||
|
||||
NcmContentMetaHeader header{};
|
||||
NcmContentMetaHeader meta_header{};
|
||||
NcmContentMetaKey key{};
|
||||
NcmContentInfo content_info{};
|
||||
std::vector<u8> extended_header{};
|
||||
@@ -107,7 +78,7 @@ struct ThreadBuffer {
|
||||
buf.reserve(INFLATE_BUFFER_MAX);
|
||||
}
|
||||
|
||||
PageAlignedVector buf;
|
||||
std::vector<u8> buf;
|
||||
s64 off;
|
||||
};
|
||||
|
||||
@@ -137,7 +108,7 @@ public:
|
||||
return ringbuf_capacity() - ringbuf_size();
|
||||
}
|
||||
|
||||
void ringbuf_push(PageAlignedVector& buf_in, s64 off_in) {
|
||||
void ringbuf_push(std::vector<u8>& buf_in, s64 off_in) {
|
||||
auto& value = this->buf[this->w_index % ringbuf_capacity()];
|
||||
value.off = off_in;
|
||||
std::swap(value.buf, buf_in);
|
||||
@@ -145,7 +116,7 @@ public:
|
||||
this->w_index = (this->w_index + 1U) % (ringbuf_capacity() * 2U);
|
||||
}
|
||||
|
||||
void ringbuf_pop(PageAlignedVector& buf_out, s64& off_out) {
|
||||
void ringbuf_pop(std::vector<u8>& buf_out, s64& off_out) {
|
||||
auto& value = this->buf[this->r_index % ringbuf_capacity()];
|
||||
off_out = value.off;
|
||||
std::swap(value.buf, buf_out);
|
||||
@@ -178,7 +149,7 @@ struct ThreadData {
|
||||
|
||||
Result Read(void* buf, s64 size, u64* bytes_read);
|
||||
|
||||
Result SetDecompressBuf(PageAlignedVector& buf, s64 off, s64 size) {
|
||||
Result SetDecompressBuf(std::vector<u8>& buf, s64 off, s64 size) {
|
||||
buf.resize(size);
|
||||
|
||||
mutexLock(std::addressof(read_mutex));
|
||||
@@ -192,7 +163,7 @@ struct ThreadData {
|
||||
return condvarWakeOne(std::addressof(can_decompress));
|
||||
}
|
||||
|
||||
Result GetDecompressBuf(PageAlignedVector& buf_out, s64& off_out) {
|
||||
Result GetDecompressBuf(std::vector<u8>& buf_out, s64& off_out) {
|
||||
mutexLock(std::addressof(read_mutex));
|
||||
if (!read_buffers.ringbuf_size()) {
|
||||
R_TRY(condvarWait(std::addressof(can_decompress), std::addressof(read_mutex)));
|
||||
@@ -204,7 +175,7 @@ struct ThreadData {
|
||||
return condvarWakeOne(std::addressof(can_read));
|
||||
}
|
||||
|
||||
Result SetWriteBuf(PageAlignedVector& buf, s64 size, bool skip_verify) {
|
||||
Result SetWriteBuf(std::vector<u8>& buf, s64 size, bool skip_verify) {
|
||||
buf.resize(size);
|
||||
if (!skip_verify) {
|
||||
sha256ContextUpdate(std::addressof(sha256), buf.data(), buf.size());
|
||||
@@ -221,7 +192,7 @@ struct ThreadData {
|
||||
return condvarWakeOne(std::addressof(can_write));
|
||||
}
|
||||
|
||||
Result GetWriteBuf(PageAlignedVector& buf_out, s64& off_out) {
|
||||
Result GetWriteBuf(std::vector<u8>& buf_out, s64& off_out) {
|
||||
mutexLock(std::addressof(write_mutex));
|
||||
if (!write_buffers.ringbuf_size()) {
|
||||
R_TRY(condvarWait(std::addressof(can_write), std::addressof(write_mutex)));
|
||||
@@ -276,8 +247,8 @@ struct Yati {
|
||||
|
||||
Result Setup(const ConfigOverride& override);
|
||||
Result InstallNca(std::span<TikCollection> tickets, NcaCollection& nca);
|
||||
Result InstallNcaInternal(std::span<TikCollection> tickets, NcaCollection& nca);
|
||||
Result InstallCnmtNca(std::span<TikCollection> tickets, CnmtCollection& cnmt, const container::Collections& collections);
|
||||
Result InstallControlNca(std::span<TikCollection> tickets, const CnmtCollection& cnmt, NcaCollection& nca);
|
||||
|
||||
Result readFuncInternal(ThreadData* t);
|
||||
Result decompressFuncInternal(ThreadData* t);
|
||||
@@ -361,11 +332,11 @@ HashStr hexIdToStr(auto id) {
|
||||
// parsing ncz headers, sections and reading ncz blocks
|
||||
Result Yati::readFuncInternal(ThreadData* t) {
|
||||
// the main buffer which data is read into.
|
||||
PageAlignedVector buf;
|
||||
std::vector<u8> buf;
|
||||
// workaround ncz block reading ahead. if block isn't found, we usually
|
||||
// would seek back to the offset, however this is not possible in stream
|
||||
// mode, so we instead store the data to the temp buffer and pre-pend it.
|
||||
PageAlignedVector temp_buf;
|
||||
std::vector<u8> temp_buf;
|
||||
buf.reserve(t->max_buffer_size);
|
||||
temp_buf.reserve(t->max_buffer_size);
|
||||
|
||||
@@ -453,12 +424,12 @@ Result Yati::decompressFuncInternal(ThreadData* t) {
|
||||
|
||||
s64 inflate_offset{};
|
||||
Aes128CtrContext ctx{};
|
||||
PageAlignedVector inflate_buf{};
|
||||
std::vector<u8> inflate_buf{};
|
||||
inflate_buf.reserve(t->max_buffer_size);
|
||||
|
||||
s64 written{};
|
||||
s64 decompress_buf_off{};
|
||||
PageAlignedVector buf{};
|
||||
std::vector<u8> buf{};
|
||||
buf.reserve(t->max_buffer_size);
|
||||
|
||||
// encrypts the nca and passes the buffer to the write thread.
|
||||
@@ -471,7 +442,7 @@ Result Yati::decompressFuncInternal(ThreadData* t) {
|
||||
// the remaining data.
|
||||
// rather that copying the entire vector to the write thread,
|
||||
// only copy (store) the remaining amount.
|
||||
PageAlignedVector temp_vector{};
|
||||
std::vector<u8> temp_vector{};
|
||||
if (size < inflate_offset) {
|
||||
temp_vector.resize(inflate_offset - size);
|
||||
std::memcpy(temp_vector.data(), inflate_buf.data() + size, temp_vector.size());
|
||||
@@ -538,6 +509,9 @@ Result Yati::decompressFuncInternal(ThreadData* t) {
|
||||
R_UNLESS(header.magic == 0x3341434E, Result_InvalidNcaMagic);
|
||||
log_write("nca magic is ok! type: %u\n", header.content_type);
|
||||
|
||||
// store the unmodified header.
|
||||
t->nca->header = header;
|
||||
|
||||
if (!config.skip_rsa_header_fixed_key_verify) {
|
||||
log_write("verifying nca fixed key\n");
|
||||
R_TRY(nca::VerifyFixedKey(header));
|
||||
@@ -556,7 +530,7 @@ Result Yati::decompressFuncInternal(ThreadData* t) {
|
||||
|
||||
TikCollection* ticket = nullptr;
|
||||
if (isRightsIdValid(header.rights_id)) {
|
||||
auto it = std::find_if(t->tik.begin(), t->tik.end(), [header](auto& e){
|
||||
auto it = std::find_if(t->tik.begin(), t->tik.end(), [&header](auto& e){
|
||||
return !std::memcmp(&header.rights_id, &e.rights_id, sizeof(e.rights_id));
|
||||
});
|
||||
|
||||
@@ -709,7 +683,7 @@ Result Yati::decompressFuncInternal(ThreadData* t) {
|
||||
|
||||
// write thread writes data to the nca placeholder.
|
||||
Result Yati::writeFuncInternal(ThreadData* t) {
|
||||
PageAlignedVector buf;
|
||||
std::vector<u8> buf;
|
||||
buf.reserve(t->max_buffer_size);
|
||||
|
||||
while (t->write_offset < t->write_size && R_SUCCEEDED(t->GetResults())) {
|
||||
@@ -790,10 +764,15 @@ Yati::~Yati() {
|
||||
|
||||
serviceClose(std::addressof(es));
|
||||
appletSetMediaPlaybackState(false);
|
||||
|
||||
if (config.boost_mode) {
|
||||
appletSetCpuBoostMode(ApmCpuBoostMode_Normal);
|
||||
}
|
||||
}
|
||||
|
||||
Result Yati::Setup(const ConfigOverride& override) {
|
||||
config.sd_card_install = override.sd_card_install.value_or(App::GetApp()->m_install_sd.Get());
|
||||
config.boost_mode = App::GetApp()->m_boost_mode.Get();
|
||||
config.allow_downgrade = App::GetApp()->m_allow_downgrade.Get();
|
||||
config.skip_if_already_installed = App::GetApp()->m_skip_if_already_installed.Get();
|
||||
config.ticket_only = App::GetApp()->m_ticket_only.Get();
|
||||
@@ -811,6 +790,10 @@ Result Yati::Setup(const ConfigOverride& override) {
|
||||
config.lower_system_version = override.lower_system_version.value_or(App::GetApp()->m_lower_system_version.Get());
|
||||
storage_id = config.sd_card_install ? NcmStorageId_SdCard : NcmStorageId_BuiltInUser;
|
||||
|
||||
if (config.boost_mode) {
|
||||
appletSetCpuBoostMode(ApmCpuBoostMode_FastLoad);
|
||||
}
|
||||
|
||||
R_TRY(source->GetOpenResult());
|
||||
R_TRY(splCryptoInitialize());
|
||||
R_TRY(nsInitialize());
|
||||
@@ -829,10 +812,17 @@ Result Yati::Setup(const ConfigOverride& override) {
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result Yati::InstallNca(std::span<TikCollection> tickets, NcaCollection& nca) {
|
||||
log_write("in install nca\n");
|
||||
pbox->NewTransfer(nca.name);
|
||||
keys::parse_hex_key(std::addressof(nca.content_id), nca.name.c_str());
|
||||
Result Yati::InstallNcaInternal(std::span<TikCollection> tickets, NcaCollection& nca) {
|
||||
if (config.skip_if_already_installed) {
|
||||
R_TRY(ncmContentStorageHas(std::addressof(cs), std::addressof(nca.skipped), std::addressof(nca.content_id)));
|
||||
if (nca.skipped) {
|
||||
log_write("\tskipped nca as it's already installed ncmContentStorageHas()\n");
|
||||
R_TRY(ncmContentStorageReadContentIdFile(std::addressof(cs), std::addressof(nca.header), sizeof(nca.header), std::addressof(nca.content_id), 0));
|
||||
crypto::cryptoAes128Xts(std::addressof(nca.header), std::addressof(nca.header), keys.header_key, 0, 0x200, sizeof(nca.header), false);
|
||||
R_SUCCEED();
|
||||
}
|
||||
}
|
||||
|
||||
log_write("generateing placeholder\n");
|
||||
R_TRY(ncmContentStorageGeneratePlaceHolderId(std::addressof(cs), std::addressof(nca.placeholder_id)));
|
||||
log_write("creating placeholder\n");
|
||||
@@ -918,39 +908,71 @@ Result Yati::InstallNca(std::span<TikCollection> tickets, NcaCollection& nca) {
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result Yati::InstallNca(std::span<TikCollection> tickets, NcaCollection& nca) {
|
||||
log_write("in install nca\n");
|
||||
pbox->NewTransfer(nca.name);
|
||||
keys::parse_hex_key(std::addressof(nca.content_id), nca.name.c_str());
|
||||
|
||||
R_TRY(InstallNcaInternal(tickets, nca));
|
||||
|
||||
fs::FsPath path;
|
||||
if (nca.skipped) {
|
||||
R_TRY(ncmContentStorageGetPath(std::addressof(cs), path, sizeof(path), std::addressof(nca.content_id)));
|
||||
} else {
|
||||
R_TRY(ncmContentStorageFlushPlaceHolder(std::addressof(cs)));
|
||||
R_TRY(ncmContentStorageGetPlaceHolderPath(std::addressof(cs), path, sizeof(path), std::addressof(nca.placeholder_id)));
|
||||
}
|
||||
|
||||
if (nca.header.content_type == nca::ContentType_Program) {
|
||||
// todo: verify npdm key.
|
||||
} else if (nca.header.content_type == nca::ContentType_Control) {
|
||||
NacpLanguageEntry entry;
|
||||
std::vector<u8> icon;
|
||||
R_TRY(yati::ParseControlNca(path, nca.header.program_id, &entry, sizeof(entry), &icon));
|
||||
pbox->SetTitle(entry.name).SetImageData(icon);
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result Yati::InstallCnmtNca(std::span<TikCollection> tickets, CnmtCollection& cnmt, const container::Collections& collections) {
|
||||
R_TRY(InstallNca(tickets, cnmt));
|
||||
|
||||
fs::FsPath path;
|
||||
R_TRY(ncmContentStorageFlushPlaceHolder(std::addressof(cs)));
|
||||
R_TRY(ncmContentStorageGetPlaceHolderPath(std::addressof(cs), path, sizeof(path), std::addressof(cnmt.placeholder_id)));
|
||||
if (cnmt.skipped) {
|
||||
R_TRY(ncmContentStorageGetPath(std::addressof(cs), path, sizeof(path), std::addressof(cnmt.content_id)));
|
||||
} else {
|
||||
R_TRY(ncmContentStorageFlushPlaceHolder(std::addressof(cs)));
|
||||
R_TRY(ncmContentStorageGetPlaceHolderPath(std::addressof(cs), path, sizeof(path), std::addressof(cnmt.placeholder_id)));
|
||||
}
|
||||
|
||||
ncm::PackagedContentMeta header;
|
||||
std::vector<NcmPackagedContentInfo> infos;
|
||||
R_TRY(ParseCnmtNca(path, header, cnmt.extended_header, infos));
|
||||
R_TRY(ParseCnmtNca(path, cnmt.header.program_id, header, cnmt.extended_header, infos));
|
||||
|
||||
for (const auto& info : infos) {
|
||||
if (info.info.content_type == NcmContentType_DeltaFragment) {
|
||||
for (const auto& packed_info : infos) {
|
||||
const auto& info = packed_info.info;
|
||||
if (info.content_type == NcmContentType_DeltaFragment) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto str = hexIdToStr(info.info.content_id);
|
||||
const auto it = std::find_if(collections.cbegin(), collections.cend(), [str](auto& e){
|
||||
const auto str = hexIdToStr(info.content_id);
|
||||
const auto it = std::find_if(collections.cbegin(), collections.cend(), [&str](auto& e){
|
||||
return e.name.find(str.str) != e.name.npos;
|
||||
});
|
||||
|
||||
R_UNLESS(it != collections.cend(), Result_NcaNotFound);
|
||||
|
||||
log_write("found: %s\n", str.str);
|
||||
cnmt.infos.emplace_back(info);
|
||||
cnmt.infos.emplace_back(packed_info);
|
||||
auto& nca = cnmt.ncas.emplace_back(*it);
|
||||
nca.type = info.info.content_type;
|
||||
nca.type = info.content_type;
|
||||
}
|
||||
|
||||
// update header
|
||||
cnmt.header = header.meta_header;
|
||||
cnmt.header.content_count = cnmt.infos.size() + 1;
|
||||
cnmt.header.storage_id = 0;
|
||||
cnmt.meta_header = header.meta_header;
|
||||
cnmt.meta_header.content_count = cnmt.infos.size() + 1;
|
||||
cnmt.meta_header.storage_id = 0;
|
||||
|
||||
cnmt.key.id = header.title_id;
|
||||
cnmt.key.version = header.title_version;
|
||||
@@ -985,26 +1007,6 @@ Result Yati::InstallCnmtNca(std::span<TikCollection> tickets, CnmtCollection& cn
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result Yati::InstallControlNca(std::span<TikCollection> tickets, const CnmtCollection& cnmt, NcaCollection& nca) {
|
||||
R_TRY(InstallNca(tickets, nca));
|
||||
|
||||
fs::FsPath path;
|
||||
R_TRY(ncmContentStorageFlushPlaceHolder(std::addressof(cs)));
|
||||
R_TRY(ncmContentStorageGetPlaceHolderPath(std::addressof(cs), path, sizeof(path), std::addressof(nca.placeholder_id)));
|
||||
|
||||
// this can fail if it's not a valid control nca, examples are mario 3d all stars.
|
||||
// there are 4 control ncas, only 1 is valid (InvalidNcaId 0x235E02).
|
||||
NacpLanguageEntry entry;
|
||||
std::vector<u8> icon;
|
||||
if (R_SUCCEEDED(yati::ParseControlNca(path, ncm::GetAppId(cnmt.key), &entry, sizeof(entry), &icon))) {
|
||||
pbox->SetTitle(entry.name).SetImageData(icon);
|
||||
} else {
|
||||
log_write("\tWARNING: failed to parse control nca!\n");
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result Yati::ParseTicketsIntoCollection(std::vector<TikCollection>& tickets, const container::Collections& collections, bool read_data) {
|
||||
for (const auto& collection : collections) {
|
||||
if (collection.name.ends_with(".tik")) {
|
||||
@@ -1012,7 +1014,7 @@ Result Yati::ParseTicketsIntoCollection(std::vector<TikCollection>& tickets, con
|
||||
keys::parse_hex_key(entry.rights_id.c, collection.name.c_str());
|
||||
const auto str = collection.name.substr(0, collection.name.length() - 4) + ".cert";
|
||||
|
||||
const auto cert = std::find_if(collections.cbegin(), collections.cend(), [str](auto& e){
|
||||
const auto cert = std::find_if(collections.cbegin(), collections.cend(), [&str](auto& e){
|
||||
return e.name.find(str) != e.name.npos;
|
||||
});
|
||||
|
||||
@@ -1091,6 +1093,13 @@ Result Yati::ShouldSkip(const CnmtCollection& cnmt, bool& skip) {
|
||||
} else if (config.skip_data_patch && cnmt.key.type == NcmContentMetaType_DataPatch) {
|
||||
log_write("\tskipping: [NcmContentMetaType_DataPatch]\n");
|
||||
skip = true;
|
||||
} else if (config.skip_if_already_installed) {
|
||||
bool has;
|
||||
R_TRY(ncmContentMetaDatabaseHas(std::addressof(db), std::addressof(has), std::addressof(cnmt.key)));
|
||||
if (has) {
|
||||
log_write("\tskipping: [ncmContentMetaDatabaseHas()]\n");
|
||||
skip = true;
|
||||
}
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
@@ -1183,7 +1192,7 @@ Result Yati::RegisterNcasAndPushRecord(const CnmtCollection& cnmt, u32 latest_ve
|
||||
log_write("registered cnmt nca\n");
|
||||
|
||||
for (auto& nca : cnmt.ncas) {
|
||||
if (nca.type != NcmContentType_DeltaFragment) {
|
||||
if (!nca.skipped && nca.type != NcmContentType_DeltaFragment) {
|
||||
log_write("registering nca: %s\n", nca.name.c_str());
|
||||
R_TRY(ncm::Register(std::addressof(cs), std::addressof(nca.content_id), std::addressof(nca.placeholder_id)));
|
||||
log_write("registered nca: %s\n", nca.name.c_str());
|
||||
@@ -1194,7 +1203,7 @@ Result Yati::RegisterNcasAndPushRecord(const CnmtCollection& cnmt, u32 latest_ve
|
||||
|
||||
// build ncm meta and push to the database.
|
||||
BufHelper buf{};
|
||||
buf.write(std::addressof(cnmt.header), sizeof(cnmt.header));
|
||||
buf.write(std::addressof(cnmt.meta_header), sizeof(cnmt.meta_header));
|
||||
buf.write(cnmt.extended_header.data(), cnmt.extended_header.size());
|
||||
buf.write(std::addressof(cnmt.content_info), sizeof(cnmt.content_info));
|
||||
|
||||
@@ -1262,12 +1271,7 @@ Result InstallInternal(ui::ProgressBox* pbox, std::shared_ptr<source::Base> sour
|
||||
|
||||
log_write("installing nca's\n");
|
||||
for (auto& nca : cnmt.ncas) {
|
||||
if (nca.type == NcmContentType_Control) {
|
||||
log_write("installing control nca\n");
|
||||
R_TRY(yati->InstallControlNca(tickets, cnmt, nca));
|
||||
} else {
|
||||
R_TRY(yati->InstallNca(tickets, nca));
|
||||
}
|
||||
R_TRY(yati->InstallNca(tickets, nca));
|
||||
}
|
||||
|
||||
R_TRY(yati->ImportTickets(tickets));
|
||||
@@ -1284,6 +1288,7 @@ Result InstallInternalStream(ui::ProgressBox* pbox, std::shared_ptr<source::Base
|
||||
R_TRY(yati->Setup(override));
|
||||
|
||||
// not supported with stream installs (yet).
|
||||
yati->config.skip_if_already_installed = false;
|
||||
yati->config.convert_to_standard_crypto = false;
|
||||
yati->config.lower_master_key = false;
|
||||
|
||||
@@ -1326,7 +1331,7 @@ Result InstallInternalStream(ui::ProgressBox* pbox, std::shared_ptr<source::Base
|
||||
keys::parse_hex_key(rights_id.c, collection.name.c_str());
|
||||
const auto str = collection.name.substr(0, collection.name.length() - 4) + ".cert";
|
||||
|
||||
auto entry = std::find_if(tickets.begin(), tickets.end(), [rights_id](auto& e){
|
||||
auto entry = std::find_if(tickets.begin(), tickets.end(), [&rights_id](auto& e){
|
||||
return !std::memcmp(&rights_id, &e.rights_id, sizeof(rights_id));
|
||||
});
|
||||
|
||||
@@ -1345,7 +1350,7 @@ Result InstallInternalStream(ui::ProgressBox* pbox, std::shared_ptr<source::Base
|
||||
for (auto& cnmt : cnmts) {
|
||||
// copy nca structs into cnmt.
|
||||
for (auto& cnmt_nca : cnmt.ncas) {
|
||||
auto it = std::find_if(ncas.cbegin(), ncas.cend(), [cnmt_nca](auto& e){
|
||||
auto it = std::find_if(ncas.cbegin(), ncas.cend(), [&cnmt_nca](auto& e){
|
||||
return e.name == cnmt_nca.name;
|
||||
});
|
||||
|
||||
@@ -1412,9 +1417,9 @@ Result InstallFromCollections(ui::ProgressBox* pbox, std::shared_ptr<source::Bas
|
||||
}
|
||||
}
|
||||
|
||||
Result ParseCnmtNca(const fs::FsPath& path, ncm::PackagedContentMeta& header, std::vector<u8>& extended_header, std::vector<NcmPackagedContentInfo>& infos) {
|
||||
Result ParseCnmtNca(const fs::FsPath& path, u64 program_id, ncm::PackagedContentMeta& header, std::vector<u8>& extended_header, std::vector<NcmPackagedContentInfo>& infos) {
|
||||
FsFileSystem fs;
|
||||
R_TRY(fsOpenFileSystem(std::addressof(fs), FsFileSystemType_ContentMeta, path));
|
||||
R_TRY(fsOpenFileSystemWithId(std::addressof(fs), program_id, FsFileSystemType_ContentMeta, path, FsContentAttributes_All));
|
||||
ON_SCOPE_EXIT(fsFsClose(std::addressof(fs)));
|
||||
|
||||
FsDir dir;
|
||||
@@ -1447,9 +1452,9 @@ Result ParseCnmtNca(const fs::FsPath& path, ncm::PackagedContentMeta& header, st
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result ParseControlNca(const fs::FsPath& path, u64 id, void* nacp_out, s64 nacp_size, std::vector<u8>* icon_out) {
|
||||
Result ParseControlNca(const fs::FsPath& path, u64 program_id, void* nacp_out, s64 nacp_size, std::vector<u8>* icon_out) {
|
||||
FsFileSystem fs;
|
||||
R_TRY(fsOpenFileSystemWithId(std::addressof(fs), id, FsFileSystemType_ContentControl, path, FsContentAttributes_All));
|
||||
R_TRY(fsOpenFileSystemWithId(std::addressof(fs), program_id, FsFileSystemType_ContentControl, path, FsContentAttributes_All));
|
||||
ON_SCOPE_EXIT(fsFsClose(std::addressof(fs)));
|
||||
|
||||
// read nacp.
|
||||
|
||||
171
tools/usb_install_pc.py
Normal file
171
tools/usb_install_pc.py
Normal file
@@ -0,0 +1,171 @@
|
||||
# This script depends on PyUSB. You can get it with pip install pyusb.
|
||||
# You will also need libusb installed
|
||||
|
||||
# My sincere apologies for this process being overly complicated. Apparently Python and Windows
|
||||
# aren't very friendly :(
|
||||
# Windows Instructions:
|
||||
# 1. Download Zadig from https://zadig.akeo.ie/.
|
||||
# 2. With your switch plugged in and on the Tinfoil USB install menu,
|
||||
# choose "List All Devices" under the options menu in Zadig, and select libnx USB comms.
|
||||
# 3. Choose libusbK from the driver list and click the "Replace Driver" button.
|
||||
# 4. Run this script
|
||||
|
||||
# macOS Instructions:
|
||||
# 1. Install Homebrew https://brew.sh
|
||||
# 2. Install Python 3
|
||||
# sudo mkdir /usr/local/Frameworks
|
||||
# sudo chown $(whoami) /usr/local/Frameworks
|
||||
# brew install python
|
||||
# 3. Install PyUSB
|
||||
# pip3 install pyusb
|
||||
# 4. Install libusb
|
||||
# brew install libusb
|
||||
# 5. Plug in your Switch and go to Tinfoil > Title Management > USB Install NSP
|
||||
# 6. Run this script
|
||||
# python3 usb_install_pc.py <path/to/nsp_folder>
|
||||
|
||||
import usb.core
|
||||
import usb.util
|
||||
import struct
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import time
|
||||
|
||||
CMD_ID_EXIT = 0
|
||||
CMD_ID_FILE_RANGE = 1
|
||||
|
||||
CMD_TYPE_RESPONSE = 1
|
||||
|
||||
# list of supported extensions.
|
||||
EXTS = (".nsp", ".xci", ".nsz", ".xcz")
|
||||
|
||||
def send_response_header(out_ep, cmd_id, data_size):
|
||||
out_ep.write(b'TUC0') # Tinfoil USB Command 0
|
||||
out_ep.write(struct.pack('<B', CMD_TYPE_RESPONSE))
|
||||
out_ep.write(b'\x00' * 3)
|
||||
out_ep.write(struct.pack('<I', cmd_id))
|
||||
out_ep.write(struct.pack('<Q', data_size))
|
||||
out_ep.write(b'\x00' * 0xC)
|
||||
|
||||
def file_range_cmd(nsp_dir, in_ep, out_ep, data_size):
|
||||
file_range_header = in_ep.read(0x20)
|
||||
|
||||
range_size = struct.unpack('<Q', file_range_header[:8])[0]
|
||||
range_offset = struct.unpack('<Q', file_range_header[8:16])[0]
|
||||
nsp_name_len = struct.unpack('<Q', file_range_header[16:24])[0]
|
||||
#in_ep.read(0x8) # Reserved
|
||||
nsp_name = bytes(in_ep.read(nsp_name_len)).decode('utf-8')
|
||||
|
||||
print('Range Size: {}, Range Offset: {}, Name len: {}, Name: {}'.format(range_size, range_offset, nsp_name_len, nsp_name))
|
||||
send_response_header(out_ep, CMD_ID_FILE_RANGE, range_size)
|
||||
|
||||
with open(nsp_name, 'rb') as f:
|
||||
f.seek(range_offset)
|
||||
|
||||
curr_off = 0x0
|
||||
end_off = range_size
|
||||
read_size = 0x800000
|
||||
|
||||
while curr_off < end_off:
|
||||
if curr_off + read_size >= end_off:
|
||||
read_size = end_off - curr_off
|
||||
|
||||
buf = f.read(read_size)
|
||||
out_ep.write(data=buf, timeout=0)
|
||||
curr_off += read_size
|
||||
|
||||
def poll_commands(nsp_dir, in_ep, out_ep):
|
||||
while True:
|
||||
cmd_header = bytes(in_ep.read(0x20, timeout=0))
|
||||
magic = cmd_header[:4]
|
||||
print('Magic: {}'.format(magic), flush=True)
|
||||
|
||||
if magic != b'TUC0': # Tinfoil USB Command 0
|
||||
continue
|
||||
|
||||
cmd_type = struct.unpack('<B', cmd_header[4:5])[0]
|
||||
cmd_id = struct.unpack('<I', cmd_header[8:12])[0]
|
||||
data_size = struct.unpack('<Q', cmd_header[12:20])[0]
|
||||
|
||||
print('Cmd Type: {}, Command id: {}, Data size: {}'.format(cmd_type, cmd_id, data_size), flush=True)
|
||||
|
||||
if cmd_id == CMD_ID_EXIT:
|
||||
print('Exiting...')
|
||||
break
|
||||
elif cmd_id == CMD_ID_FILE_RANGE:
|
||||
file_range_cmd(nsp_dir, in_ep, out_ep, data_size)
|
||||
|
||||
def send_nsp_list(nsp_dir, out_ep):
|
||||
nsp_path_list = list()
|
||||
nsp_path_list_len = 0
|
||||
|
||||
# Add all files with the extension .nsp in the provided dir
|
||||
for nsp_path in [f for f in nsp_dir.iterdir() if f.is_file() and (f.suffix in EXTS)]:
|
||||
nsp_path_list.append(nsp_path.__str__() + '\n')
|
||||
nsp_path_list_len += len(nsp_path.__str__()) + 1
|
||||
|
||||
print('Sending header...')
|
||||
|
||||
out_ep.write(b'TUL0') # Tinfoil USB List 0
|
||||
out_ep.write(struct.pack('<I', nsp_path_list_len))
|
||||
out_ep.write(b'\x00' * 0x8) # Padding
|
||||
|
||||
print('Sending NSP list: {}'.format(nsp_path_list))
|
||||
|
||||
for nsp_path in nsp_path_list:
|
||||
out_ep.write(nsp_path)
|
||||
|
||||
def print_usage():
|
||||
print("""\
|
||||
usb_install_pc.py
|
||||
|
||||
Used for the installation of NSPs over USB.
|
||||
|
||||
Usage: usb_install_pc.py <nsp folder>""")
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) != 2:
|
||||
print_usage()
|
||||
sys.exit(1)
|
||||
|
||||
nsp_dir = Path(sys.argv[1])
|
||||
|
||||
if not nsp_dir.is_dir():
|
||||
raise ValueError('1st argument must be a directory')
|
||||
|
||||
print("waiting for switch...\n")
|
||||
dev = None
|
||||
|
||||
while (dev is None):
|
||||
dev = usb.core.find(idVendor=0x057E, idProduct=0x3000)
|
||||
time.sleep(0.5)
|
||||
|
||||
print("found the switch!\n")
|
||||
|
||||
cfg = None
|
||||
|
||||
try:
|
||||
cfg = dev.get_active_configuration()
|
||||
print("found active config")
|
||||
except usb.core.USBError:
|
||||
print("no currently active config")
|
||||
cfg = None
|
||||
|
||||
if cfg is None:
|
||||
dev.reset()
|
||||
dev.set_configuration()
|
||||
cfg = dev.get_active_configuration()
|
||||
|
||||
is_out_ep = lambda ep: usb.util.endpoint_direction(ep.bEndpointAddress) == usb.util.ENDPOINT_OUT
|
||||
is_in_ep = lambda ep: usb.util.endpoint_direction(ep.bEndpointAddress) == usb.util.ENDPOINT_IN
|
||||
out_ep = usb.util.find_descriptor(cfg[(0,0)], custom_match=is_out_ep)
|
||||
in_ep = usb.util.find_descriptor(cfg[(0,0)], custom_match=is_in_ep)
|
||||
|
||||
assert out_ep is not None
|
||||
assert in_ep is not None
|
||||
|
||||
print("iManufacturer: {} iProduct: {} iSerialNumber: {}".format(dev.manufacturer, dev.product, dev.serial_number))
|
||||
print("bcdUSB: {} bMaxPacketSize0: {}".format(hex(dev.bcdUSB), dev.bMaxPacketSize0))
|
||||
|
||||
send_nsp_list(nsp_dir, out_ep)
|
||||
poll_commands(nsp_dir, in_ep, out_ep)
|
||||
@@ -1,141 +0,0 @@
|
||||
# based on usb.py from Tinfoil, by Adubbz.
|
||||
import struct
|
||||
import sys
|
||||
import os
|
||||
import usb.core
|
||||
import usb.util
|
||||
import time
|
||||
import glob
|
||||
from pathlib import Path
|
||||
|
||||
# magic number (SPHA) for the script and switch.
|
||||
MAGIC = 0x53504841
|
||||
# version of the usb script.
|
||||
VERSION = 2
|
||||
# list of supported extensions.
|
||||
EXTS = (".nsp", ".xci", ".nsz", ".xcz")
|
||||
|
||||
def verify_switch(bcdUSB, count, in_ep, out_ep):
|
||||
header = in_ep.read(8, timeout=0)
|
||||
switch_magic = struct.unpack('<I', header[0:4])[0]
|
||||
switch_version = struct.unpack('<I', header[4:8])[0]
|
||||
|
||||
if switch_magic != MAGIC:
|
||||
raise Exception("Unexpected magic {}".format(switch_magic))
|
||||
|
||||
if switch_version != VERSION:
|
||||
raise Exception("Unexpected version {}".format(switch_version))
|
||||
|
||||
send_data = struct.pack('<IIII', MAGIC, VERSION, bcdUSB, count)
|
||||
out_ep.write(data=send_data, timeout=0)
|
||||
|
||||
def send_file_info(path, in_ep, out_ep):
|
||||
file_name = Path(path).name
|
||||
file_size = Path(path).stat().st_size
|
||||
file_name_len = len(file_name)
|
||||
|
||||
send_data = struct.pack('<QQ', file_size, file_name_len)
|
||||
out_ep.write(data=send_data, timeout=0)
|
||||
out_ep.write(data=file_name, timeout=0)
|
||||
|
||||
def wait_for_input(path, in_ep, out_ep):
|
||||
buf = None
|
||||
predicted_off = 0
|
||||
print("now waiting for intput\n")
|
||||
|
||||
with open(path, "rb") as file:
|
||||
while True:
|
||||
header = in_ep.read(24, timeout=0)
|
||||
|
||||
range_offset = struct.unpack('<Q', header[8:16])[0]
|
||||
range_size = struct.unpack('<Q', header[16:24])[0]
|
||||
|
||||
if (range_offset == 0 and range_size == 0):
|
||||
break
|
||||
|
||||
if (buf != None and range_offset == predicted_off and range_size == len(buf)):
|
||||
# print("predicted the read off {} size {}".format(predicted_off, len(buf)))
|
||||
pass
|
||||
else:
|
||||
file.seek(range_offset)
|
||||
buf = file.read(range_size)
|
||||
|
||||
if (len(buf) != range_size):
|
||||
# print("off: {} size: {}".format(range_offset, range_size))
|
||||
raise ValueError('bad buf size!!!!!')
|
||||
|
||||
result = out_ep.write(data=buf, timeout=0)
|
||||
if (len(buf) != result):
|
||||
print("off: {} size: {}".format(range_offset, range_size))
|
||||
raise ValueError('bad result!!!!!')
|
||||
|
||||
predicted_off = range_offset + range_size
|
||||
buf = file.read(range_size)
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("hello world")
|
||||
|
||||
# check which mode the user has selected.
|
||||
args = len(sys.argv)
|
||||
if (args != 2):
|
||||
print("either run python usb_total.py game.nsp OR drag and drop the game onto the python file (if python is in your path)")
|
||||
sys.exit(1)
|
||||
|
||||
path = sys.argv[1]
|
||||
files = []
|
||||
|
||||
if os.path.isfile(path) and path.endswith(EXTS):
|
||||
files.append(path)
|
||||
elif os.path.isdir(path):
|
||||
for f in glob.glob(path + "/**/*.*", recursive=True):
|
||||
if os.path.isfile(f) and f.endswith(EXTS):
|
||||
files.append(f)
|
||||
else:
|
||||
raise ValueError('must be a file!')
|
||||
|
||||
# for file in files:
|
||||
# print("found file: {}".format(file))
|
||||
|
||||
# Find the switch
|
||||
print("waiting for switch...\n")
|
||||
dev = None
|
||||
|
||||
while (dev is None):
|
||||
dev = usb.core.find(idVendor=0x057E, idProduct=0x3000)
|
||||
time.sleep(0.5)
|
||||
|
||||
print("found the switch!\n")
|
||||
|
||||
cfg = None
|
||||
|
||||
try:
|
||||
cfg = dev.get_active_configuration()
|
||||
print("found active config")
|
||||
except usb.core.USBError:
|
||||
print("no currently active config")
|
||||
cfg = None
|
||||
|
||||
if cfg is None:
|
||||
dev.set_configuration()
|
||||
cfg = dev.get_active_configuration()
|
||||
|
||||
is_out_ep = lambda ep: usb.util.endpoint_direction(ep.bEndpointAddress) == usb.util.ENDPOINT_OUT
|
||||
is_in_ep = lambda ep: usb.util.endpoint_direction(ep.bEndpointAddress) == usb.util.ENDPOINT_IN
|
||||
out_ep = usb.util.find_descriptor(cfg[(0,0)], custom_match=is_out_ep)
|
||||
in_ep = usb.util.find_descriptor(cfg[(0,0)], custom_match=is_in_ep)
|
||||
assert out_ep is not None
|
||||
assert in_ep is not None
|
||||
|
||||
print("iManufacturer: {} iProduct: {} iSerialNumber: {}".format(dev.manufacturer, dev.product, dev.serial_number))
|
||||
print("bcdUSB: {} bMaxPacketSize0: {}".format(hex(dev.bcdUSB), dev.bMaxPacketSize0))
|
||||
|
||||
try:
|
||||
verify_switch(dev.bcdUSB, len(files), in_ep, out_ep)
|
||||
|
||||
for file in files:
|
||||
print("installing file: {}".format(file))
|
||||
send_file_info(file, in_ep, out_ep)
|
||||
wait_for_input(file, in_ep, out_ep)
|
||||
dev.reset()
|
||||
except Exception as inst:
|
||||
print("An exception occurred " + str(inst))
|
||||
Reference in New Issue
Block a user