14 Commits
0.8.0 ... 0.8.1

Author SHA1 Message Date
ITotalJustice
8ffaa56bc3 bump version for new release 0.8.0 -> 0.8.1 2025-04-29 20:03:23 +01:00
ITotalJustice
eca3358e57 add option to download default music. 2025-04-29 20:01:51 +01:00
ITotalJustice
757e380e08 play sound effect when gamecard is inserted. 2025-04-29 19:23:37 +01:00
ITotalJustice
6c1b5de932 label the shortcut for misc 2025-04-29 19:06:33 +01:00
ITotalJustice
d79ac126f7 remove all strings and error codes in error_box.cpp, reduce binary by a further 60k. 2025-04-29 18:56:55 +01:00
ITotalJustice
2d7763444e remove Web menu
the web browser on the switch is really bad, it shouldnt be used.
i am removing this menu because its another option that gets in the way of other options, and code bloat.
2025-04-29 18:42:32 +01:00
ITotalJustice
1dafa2748c remove IRS menu
i added the irs menu when i wanted to mess around with the sensor on the joycon.
since then, i have used it a total of 0 times, and i don't think any users use it either.
2025-04-29 18:37:43 +01:00
ITotalJustice
9f7bf9581c add boost mode option for installing 2025-04-29 18:33:02 +01:00
ITotalJustice
8f39acbaa2 replace usb protocol with tinfoils protocol, in order to support applications supporting said protocol.
- replace the python script with the one included with tinfoil, minor changes such as changing the supported extension,
  removing unused imports.
- tested with the included script, fluffy and ns-usbloader on linux.
  a user was unable to get it working on mac however...
- added build instructions to the readme, i think they're correct.
- added install instructions to the readme.
2025-04-29 18:11:07 +01:00
ITotalJustice
81469d0ac9 remove PageAlignedVector from yati as it's no longer needed due to previous commit.
the previous commit changed usb transfers to always transfer to/from page aligned buffers.
i wanted to keep the commits seperate so that its easier revert or git bisect later on, if needed.
2025-04-29 14:19:37 +01:00
ITotalJustice
1eae35f072 simplify the usb transfer process by using an aligned buffer to transfer to/from. 2025-04-29 14:17:12 +01:00
ITotalJustice
5b82e07b1c fix building due to previous commit 2025-04-29 13:08:32 +01:00
ITotalJustice
73886c28ae add gc event waiting, fix control nca mounting, better skip nca support.
- gamecards now wait for an event to change, rather than polling each frame.
  this reduces cpu load on core 3 slightly (3-4% less).
- my understanding of fsOpenFileSystemWithId() was wrong, i thought it used the app_id for the id param.
  turns out it needs the program id (found in the nca header), this is why mounting some control ncas
  would fail.
  fs (and ncm) have a call in 17+ to get the program id, it does so by parsing the nca header.
  in yati, we already have the header so we can avoid the call.
  for the gamecard menu, we don't. so we can parse the nca header, or use the id offset (which we already have)
  to form the program id.
- std::find_if in yati now takes args by ref rather than by value, avoid quite large copies.
- stream installs can now parse the control nca.
- if an nca is already installed, it is now skipped. this is regardless to whether it is not in ncm db.
- nca skipping is technically supported for stream installs, however it is disabled for now as there needs
  to be a way to allow for the stream to continue reading and discarding data until the stream has finished.
  currently, if a ftp (stream) install is skipped, it will close the progress box and cause spahira to hang.
  this is because sphaira expects the stream to only be closed upon all data being read, so there's nothing more
  to process.
- renamed the title_id field in nca header to program_id.
2025-04-29 12:47:38 +01:00
ITotalJustice
eea09f6e57 [appstore] make author search case insensitive. 2025-04-28 22:06:44 +01:00
25 changed files with 693 additions and 2282 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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};

View File

@@ -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

View File

@@ -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{};

View File

@@ -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

View File

@@ -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

View File

@@ -226,7 +226,6 @@ struct ThemeMeta {
struct Theme {
ThemeMeta meta;
PLSR_BFSTM music;
ElementEntry elements[ThemeEntryID_MAX];
auto GetColour(ThemeEntryID id) const {

View File

@@ -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

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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));

View File

@@ -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

View File

@@ -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);
}
}

View File

@@ -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);

View File

@@ -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

View File

@@ -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));

View File

@@ -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:

View File

@@ -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

View File

@@ -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();
}

View File

@@ -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
View 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)

View File

@@ -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))