diff --git a/Makefile b/Makefile index 567d756..94822ed 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ MAKEFILES := sysmod overlay TARGETS := $(foreach dir,$(MAKEFILES),$(CURDIR)/$(dir)) # the below was taken from atmosphere + switch-examples makefile -export VERSION := 1.6.0 +export VERSION := 1.6.1 ifneq ($(strip $(shell git symbolic-ref --short HEAD 2>/dev/null)),) export GIT_BRANCH := $(shell git symbolic-ref --short HEAD) diff --git a/overlay/src/main.cpp b/overlay/src/main.cpp index 1d4445c..3b6bbb5 100644 --- a/overlay/src/main.cpp +++ b/overlay/src/main.cpp @@ -1,6 +1,7 @@ #define TESLA_INIT_IMPL // If you have more than one file using the tesla header, only define this in the main one #define STBTT_STATIC #include // The Tesla Header +#include #include #include "minIni/minIni.h" @@ -9,6 +10,87 @@ namespace { constexpr auto CONFIG_PATH = "/config/sys-patch/config.ini"; constexpr auto LOG_PATH = "/config/sys-patch/log.ini"; +auto split_log_value(std::string_view value) -> std::pair { + if (value.empty()) { + return {{}, {}}; + } + + const auto offset_start = value.rfind(" (0x"); + if (offset_start == std::string_view::npos || value.back() != ')') { + return {std::string{value}, {}}; + } + + return { + std::string{value.substr(0, offset_start)}, + std::string{value.substr(offset_start + 1, value.size() - offset_start - 2)} + }; +} + +class ColouredListItem : public tsl::elm::ListItem { +public: + ColouredListItem(const std::string& text, const std::string& value, tsl::Color value_colour) + : tsl::elm::ListItem(text, value), m_value_colour(value_colour) { + } + + void draw(tsl::gfx::Renderer *renderer) override { + if (this->m_touched && Element::getInputMode() == tsl::InputMode::Touch) { + renderer->drawRect(ELEMENT_BOUNDS(this), a(tsl::style::color::ColorClickAnimation)); + } + + if (this->m_maxWidth == 0) { + if (!this->m_value.empty()) { + auto [valueWidth, valueHeight] = renderer->drawString(this->m_value.c_str(), false, 0, 0, 20, tsl::style::color::ColorTransparent); + this->m_maxWidth = this->getWidth() - valueWidth - 70; + } else { + this->m_maxWidth = this->getWidth() - 40; + } + + auto [width, height] = renderer->drawString(this->m_text.c_str(), false, 0, 0, 23, tsl::style::color::ColorTransparent); + this->m_trunctuated = width > this->m_maxWidth; + + if (this->m_trunctuated) { + this->m_scrollText = this->m_text + " "; + auto [scrollWidth, scrollHeight] = renderer->drawString(this->m_scrollText.c_str(), false, 0, 0, 23, tsl::style::color::ColorTransparent); + this->m_scrollText += this->m_text; + this->m_textWidth = scrollWidth; + this->m_ellipsisText = renderer->limitStringLength(this->m_text, false, 22, this->m_maxWidth); + } else { + this->m_textWidth = width; + } + } + + renderer->drawRect(this->getX(), this->getY(), this->getWidth(), 1, a(tsl::style::color::ColorFrame)); + renderer->drawRect(this->getX(), this->getTopBound(), this->getWidth(), 1, a(tsl::style::color::ColorFrame)); + + if (this->m_trunctuated) { + if (this->m_focused) { + renderer->enableScissoring(this->getX(), this->getY(), this->m_maxWidth + 40, this->getHeight()); + renderer->drawString(this->m_scrollText.c_str(), false, this->getX() + 20 - this->m_scrollOffset, this->getY() + 45, 23, tsl::style::color::ColorText); + renderer->disableScissoring(); + if (this->m_scrollAnimationCounter == 90) { + if (this->m_scrollOffset == this->m_textWidth) { + this->m_scrollOffset = 0; + this->m_scrollAnimationCounter = 0; + } else { + this->m_scrollOffset++; + } + } else { + this->m_scrollAnimationCounter++; + } + } else { + renderer->drawString(this->m_ellipsisText.c_str(), false, this->getX() + 20, this->getY() + 45, 23, a(tsl::style::color::ColorText)); + } + } else { + renderer->drawString(this->m_text.c_str(), false, this->getX() + 20, this->getY() + 45, 23, a(tsl::style::color::ColorText)); + } + + renderer->drawString(this->m_value.c_str(), false, this->getX() + this->m_maxWidth + 45, this->getY() + 45, 20, a(m_value_colour)); + } + +private: + tsl::Color m_value_colour; +}; + auto does_file_exist(const char* path) -> bool { Result rc{}; FsFileSystem fs{}; @@ -120,6 +202,9 @@ class GuiToggle final : public tsl::Gui { list->addItem(config_es4.create_list_item("es_19.0.0-21.2.0")); list->addItem(config_es5.create_list_item("es_22.0.0+")); + list->addItem(new tsl::elm::CategoryHeader("AM - 0100000000000023")); + list->addItem(config_am1.create_list_item("am_homebrew_fix_22.0.0+")); + list->addItem(new tsl::elm::CategoryHeader("OLSC - 010000000000003E")); list->addItem(config_olsc1.create_list_item("olsc_6.0.0-14.1.2")); list->addItem(config_olsc2.create_list_item("olsc_15.0.0-18.1.0")); @@ -155,6 +240,7 @@ class GuiToggle final : public tsl::Gui { ConfigEntry config_es3{"es", "es_12.0.0-18.1.0", true}; ConfigEntry config_es4{"es", "es_19.0.0-21.2.0", true}; ConfigEntry config_es5{"es", "es_22.0.0+", true}; + ConfigEntry config_am1{"am", "am_homebrew_fix_22.0.0+ ", true}; ConfigEntry config_olsc1{"olsc", "olsc_6.0.0-14.1.2", true}; ConfigEntry config_olsc2{"olsc", "olsc_15.0.0-18.1.0", true}; ConfigEntry config_olsc3{"olsc", "olsc_19.0.0+", true}; @@ -185,8 +271,9 @@ class GuiLog final : public tsl::Gui { ini_browse([](const mTCHAR *Section, const mTCHAR *Key, const mTCHAR *Value, void *UserData){ auto user = (CallbackUser*)UserData; std::string_view value{Value}; + const auto [status, detail] = split_log_value(value); - if (value == "Skipped") { + if (status == "Skipped") { return 1; } @@ -201,22 +288,20 @@ class GuiLog final : public tsl::Gui { constexpr tsl::Color colour_unpatched{F(250), F(90), F(58), F(255)}; #undef F - if (value.starts_with("Patched")) { - auto *item = new tsl::elm::ListItem(Key); - item->setValue("Patched", true); - user->list->addItem(item); - } else if (value.starts_with("Unpatched") || value.starts_with("Disabled")) { - auto *item = new tsl::elm::ListItem(Key); - item->setValue(Value, true); - user->list->addItem(item); + if (status.starts_with("Patched")) { + const auto is_sys_patch = status.ends_with("(sys-patch)"); + const auto display_value = detail.empty() ? std::string{"Patched"} : "Patched @ " + detail; + user->list->addItem(new ColouredListItem( + Key, + display_value, + is_sys_patch ? colour_syspatch : colour_file + )); + } else if (status.starts_with("Unpatched") || status.starts_with("Disabled")) { + user->list->addItem(new ColouredListItem(Key, Value, colour_unpatched)); } else if (user->last_section == "stats") { - auto *item = new tsl::elm::ListItem(Key); - item->setValue(Value, true); - user->list->addItem(item); + user->list->addItem(new ColouredListItem(Key, Value, tsl::style::color::ColorDescription)); } else { - auto *item = new tsl::elm::ListItem(Key); - item->setValue(Value, true); - user->list->addItem(item); + user->list->addItem(new ColouredListItem(Key, Value, tsl::style::color::ColorText)); } return 1; diff --git a/sysmod/src/main.cpp b/sysmod/src/main.cpp index 3c0b5ce..8d8b296 100644 --- a/sysmod/src/main.cpp +++ b/sysmod/src/main.cpp @@ -105,6 +105,7 @@ struct Patterns { const u32 max_ams_ver{FW_VER_ANY}; // set to FW_VER_ANY to ignore PatchResult result{PatchResult::NOT_FOUND}; + u64 logged_offset{}; }; struct PatchEntry { @@ -269,6 +270,10 @@ constinit Patterns es_patterns[] = { { "es_22.0.0+", "0xA0630091....FE97A08300D1....FE97", 16, 0, es_cond, mov0_patch, mov0_applied, true, MAKEHOSVERSION(22,0,0), FW_VER_ANY }, }; +constinit Patterns am_patterns[] = { + { "am_homebrew_fix_22.0.0+", "0x94......F9......F9........00410491", 17, 0, bl_cond, nop_patch, nop_applied, true, MAKEHOSVERSION(22,0,0), FW_VER_ANY }, +}; + constinit Patterns olsc_patterns[] = { { "olsc_6.0.0-14.1.2", "0x00..73....F9....4039", 42, 0, bl_cond, ret1_patch, ret1_applied, true, MAKEHOSVERSION(6,0,0), MAKEHOSVERSION(14,1,2) }, { "olsc_15.0.0-18.1.0", "0x00..73....F9....4039", 38, 0, bl_cond, ret1_patch, ret1_applied, true, MAKEHOSVERSION(15,0,0), MAKEHOSVERSION(18,1,0) }, @@ -303,6 +308,7 @@ constinit PatchEntry patches[] = { { "olsc", 0x010000000000003E, olsc_patterns, MAKEHOSVERSION(6,0,0) }, { "nifm", 0x010000000000000F, nifm_patterns }, { "nim", 0x0100000000000025, nim_patterns }, + { "am", 0x0100000000000023, am_patterns, MAKEHOSVERSION(22,0,0) }, }; struct EmummcPaths { @@ -324,7 +330,7 @@ auto is_emummc() -> bool { return (paths.unk[0] != '\0') || (paths.nintendo[0] != '\0'); } -void patcher(Handle handle, const u8* data, size_t data_size, u64 addr, std::span patterns) { +void patcher(Handle handle, const u8* data, size_t data_size, u64 addr, u64 base_addr, std::span patterns) { for (auto& p : patterns) { // skip if disabled (controller by config.ini) if (p.result == PatchResult::DISABLED) { @@ -368,10 +374,17 @@ void patcher(Handle handle, const u8* data, size_t data_size, u64 addr, std::spa const auto inst_offset = i + p.inst_offset; std::memcpy(&inst, data + inst_offset, sizeof(inst)); - // check if the instruction is the one that we want - if (p.cond(inst)) { + const auto patch_offset = addr + inst_offset + p.patch_offset; + const auto logged_offset = base_addr && patch_offset >= base_addr ? patch_offset - base_addr : patch_offset; + + // prefer detecting an already-present patch before deciding to write one + if (p.applied(data + inst_offset + p.patch_offset, inst)) { + // patch already applied by sigpatches / IPS + p.result = PatchResult::PATCHED_FILE; + p.logged_offset = logged_offset; + break; + } else if (p.cond(inst)) { const auto patch_data = p.patch(inst); - const auto patch_offset = addr + inst_offset + p.patch_offset; // todo: log failed writes, although this should in theory never fail if (R_FAILED(svcWriteDebugProcessMemory(handle, &patch_data, patch_offset, patch_data.size))) { @@ -379,11 +392,7 @@ void patcher(Handle handle, const u8* data, size_t data_size, u64 addr, std::spa } else { p.result = PatchResult::PATCHED_SYSPATCH; } - // move onto next pattern - break; - } else if (p.applied(data + inst_offset + p.patch_offset, inst)) { - // patch already applied by sigpatches - p.result = PatchResult::PATCHED_FILE; + p.logged_offset = logged_offset; break; } } @@ -422,8 +431,35 @@ auto apply_patch(PatchEntry& patch) -> bool { patch.title_id == event_info.info.create_process.program_id) { MemoryInfo mem_info{}; u64 addr{}; + u64 base_addr{}; + u64 base_size{}; u32 page_info{}; + // Log offsets relative to the main module rather than the first executable region. + for (;;) { + if (R_FAILED(svcQueryDebugProcessMemory(&mem_info, &page_info, handle, addr))) { + break; + } + addr = mem_info.addr + mem_info.size; + + // if addr=0 then we hit the reserved memory section + if (!addr) { + break; + } + // skip memory that we don't want + if (!mem_info.size || (mem_info.perm & Perm_Rx) != Perm_Rx || ((mem_info.type & 0xFF) != MemType_CodeStatic)) { + continue; + } + + if (mem_info.size > base_size) { + base_addr = mem_info.addr; + base_size = mem_info.size; + } + } + + addr = 0; + std::memset(buffer, 0, sizeof(buffer)); + for (;;) { if (R_FAILED(svcQueryDebugProcessMemory(&mem_info, &page_info, handle, addr))) { break; @@ -444,7 +480,7 @@ auto apply_patch(PatchEntry& patch) -> bool { if (R_FAILED(svcReadDebugProcessMemory(buffer + overlap_size, handle, mem_info.addr + sz, actual_size))) { break; } else { - patcher(handle, buffer, actual_size + overlap_size, mem_info.addr + sz - overlap_size, patch.patterns); + patcher(handle, buffer, actual_size + overlap_size, mem_info.addr + sz - overlap_size, base_addr, patch.patterns); if (actual_size >= overlap_size) { memcpy(buffer, buffer + READ_BUFFER_SIZE, overlap_size); std::memset(buffer + overlap_size, 0, READ_BUFFER_SIZE); @@ -506,6 +542,39 @@ auto patch_result_to_str(PatchResult result) -> const char* { std::unreachable(); } +void offset_to_str(char* s, u64 offset) { + *s++ = '0'; + *s++ = 'x'; + + bool wrote_digit = false; + for (int shift = 60; shift >= 0; shift -= 4) { + const auto nibble = static_cast((offset >> shift) & 0xF); + if (!wrote_digit && nibble == 0 && shift != 0) { + continue; + } + + wrote_digit = true; + *s++ = nibble < 10 ? static_cast('0' + nibble) : static_cast('A' + nibble - 10); + } +} + +void patch_result_to_log_str(char* s, PatchResult result, u64 offset) { + const auto* result_str = patch_result_to_str(result); + while (*result_str != '\0') { + *s++ = *result_str++; + } + + if (offset != 0) { + *s++ = ' '; + *s++ = '('; + offset_to_str(s, offset); + while (*s != '\0') { + s++; + } + *s++ = ')'; + } +} + void num_2_str(char*& s, u16 num) { u16 max_v = 1000; if (num > 9) { @@ -638,7 +707,9 @@ int main(int argc, char* argv[]) { if (!enable_patching) { p.result = PatchResult::SKIPPED; } - ini_puts(patch.name, p.patch_name, patch_result_to_str(p.result), log_path); + char log_value[96]{}; + patch_result_to_log_str(log_value, p.result, p.logged_offset); + ini_puts(patch.name, p.patch_name, log_value, log_path); } }