summaryrefslogtreecommitdiffstats
path: root/src/core/file_sys
diff options
context:
space:
mode:
authorbunnei <bunneidev@gmail.com>2019-03-22 02:41:59 +0100
committerGitHub <noreply@github.com>2019-03-22 02:41:59 +0100
commit639f0c524d3406b7c156dc75af4e934c6b5adcb6 (patch)
treee03093771bbffc832c48670331cf9af54aa4a9fc /src/core/file_sys
parentMerge pull request #2260 from lioncash/sdl (diff)
parentvm_manager: Remove cheat-specific ranges from VMManager (diff)
downloadyuzu-639f0c524d3406b7c156dc75af4e934c6b5adcb6.tar
yuzu-639f0c524d3406b7c156dc75af4e934c6b5adcb6.tar.gz
yuzu-639f0c524d3406b7c156dc75af4e934c6b5adcb6.tar.bz2
yuzu-639f0c524d3406b7c156dc75af4e934c6b5adcb6.tar.lz
yuzu-639f0c524d3406b7c156dc75af4e934c6b5adcb6.tar.xz
yuzu-639f0c524d3406b7c156dc75af4e934c6b5adcb6.tar.zst
yuzu-639f0c524d3406b7c156dc75af4e934c6b5adcb6.zip
Diffstat (limited to 'src/core/file_sys')
-rw-r--r--src/core/file_sys/cheat_engine.cpp493
-rw-r--r--src/core/file_sys/cheat_engine.h227
-rw-r--r--src/core/file_sys/patch_manager.cpp54
-rw-r--r--src/core/file_sys/patch_manager.h4
4 files changed, 778 insertions, 0 deletions
diff --git a/src/core/file_sys/cheat_engine.cpp b/src/core/file_sys/cheat_engine.cpp
new file mode 100644
index 000000000..09ca9d705
--- /dev/null
+++ b/src/core/file_sys/cheat_engine.cpp
@@ -0,0 +1,493 @@
+// Copyright 2018 yuzu emulator team
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include <locale>
+#include "common/hex_util.h"
+#include "common/microprofile.h"
+#include "common/swap.h"
+#include "core/core.h"
+#include "core/core_timing.h"
+#include "core/core_timing_util.h"
+#include "core/file_sys/cheat_engine.h"
+#include "core/hle/kernel/process.h"
+#include "core/hle/service/hid/controllers/controller_base.h"
+#include "core/hle/service/hid/controllers/npad.h"
+#include "core/hle/service/hid/hid.h"
+#include "core/hle/service/sm/sm.h"
+
+namespace FileSys {
+
+constexpr u64 CHEAT_ENGINE_TICKS = Core::Timing::BASE_CLOCK_RATE / 60;
+constexpr u32 KEYPAD_BITMASK = 0x3FFFFFF;
+
+u64 Cheat::Address() const {
+ u64 out;
+ std::memcpy(&out, raw.data(), sizeof(u64));
+ return Common::swap64(out) & 0xFFFFFFFFFF;
+}
+
+u64 Cheat::ValueWidth(u64 offset) const {
+ return Value(offset, width);
+}
+
+u64 Cheat::Value(u64 offset, u64 width) const {
+ u64 out;
+ std::memcpy(&out, raw.data() + offset, sizeof(u64));
+ out = Common::swap64(out);
+ if (width == 8)
+ return out;
+ return out & ((1ull << (width * CHAR_BIT)) - 1);
+}
+
+u32 Cheat::KeypadValue() const {
+ u32 out;
+ std::memcpy(&out, raw.data(), sizeof(u32));
+ return Common::swap32(out) & 0x0FFFFFFF;
+}
+
+void CheatList::SetMemoryParameters(VAddr main_begin, VAddr heap_begin, VAddr main_end,
+ VAddr heap_end, MemoryWriter writer, MemoryReader reader) {
+ this->main_region_begin = main_begin;
+ this->main_region_end = main_end;
+ this->heap_region_begin = heap_begin;
+ this->heap_region_end = heap_end;
+ this->writer = writer;
+ this->reader = reader;
+}
+
+MICROPROFILE_DEFINE(Cheat_Engine, "Add-Ons", "Cheat Engine", MP_RGB(70, 200, 70));
+
+void CheatList::Execute() {
+ MICROPROFILE_SCOPE(Cheat_Engine);
+
+ std::fill(scratch.begin(), scratch.end(), 0);
+ in_standard = false;
+ for (std::size_t i = 0; i < master_list.size(); ++i) {
+ LOG_DEBUG(Common_Filesystem, "Executing block #{:08X} ({})", i, master_list[i].first);
+ current_block = i;
+ ExecuteBlock(master_list[i].second);
+ }
+
+ in_standard = true;
+ for (std::size_t i = 0; i < standard_list.size(); ++i) {
+ LOG_DEBUG(Common_Filesystem, "Executing block #{:08X} ({})", i, standard_list[i].first);
+ current_block = i;
+ ExecuteBlock(standard_list[i].second);
+ }
+}
+
+CheatList::CheatList(ProgramSegment master, ProgramSegment standard)
+ : master_list(master), standard_list(standard) {}
+
+bool CheatList::EvaluateConditional(const Cheat& cheat) const {
+ using ComparisonFunction = bool (*)(u64, u64);
+ constexpr std::array<ComparisonFunction, 6> comparison_functions{
+ [](u64 a, u64 b) { return a > b; }, [](u64 a, u64 b) { return a >= b; },
+ [](u64 a, u64 b) { return a < b; }, [](u64 a, u64 b) { return a <= b; },
+ [](u64 a, u64 b) { return a == b; }, [](u64 a, u64 b) { return a != b; },
+ };
+
+ if (cheat.type == CodeType::ConditionalInput) {
+ const auto applet_resource = Core::System::GetInstance()
+ .ServiceManager()
+ .GetService<Service::HID::Hid>("hid")
+ ->GetAppletResource();
+ if (applet_resource == nullptr) {
+ LOG_WARNING(
+ Common_Filesystem,
+ "Attempted to evaluate input conditional, but applet resource is not initialized!");
+ return false;
+ }
+
+ const auto press_state =
+ applet_resource
+ ->GetController<Service::HID::Controller_NPad>(Service::HID::HidController::NPad)
+ .GetAndResetPressState();
+ return ((press_state & cheat.KeypadValue()) & KEYPAD_BITMASK) != 0;
+ }
+
+ ASSERT(cheat.type == CodeType::Conditional);
+
+ const auto offset =
+ cheat.memory_type == MemoryType::MainNSO ? main_region_begin : heap_region_begin;
+ ASSERT(static_cast<u8>(cheat.comparison_op.Value()) < 6);
+ auto* function = comparison_functions[static_cast<u8>(cheat.comparison_op.Value())];
+ const auto addr = cheat.Address() + offset;
+
+ return function(reader(cheat.width, SanitizeAddress(addr)), cheat.ValueWidth(8));
+}
+
+void CheatList::ProcessBlockPairs(const Block& block) {
+ block_pairs.clear();
+
+ u64 scope = 0;
+ std::map<u64, u64> pairs;
+
+ for (std::size_t i = 0; i < block.size(); ++i) {
+ const auto& cheat = block[i];
+
+ switch (cheat.type) {
+ case CodeType::Conditional:
+ case CodeType::ConditionalInput:
+ pairs.insert_or_assign(scope, i);
+ ++scope;
+ break;
+ case CodeType::EndConditional: {
+ --scope;
+ const auto idx = pairs.at(scope);
+ block_pairs.insert_or_assign(idx, i);
+ break;
+ }
+ case CodeType::Loop: {
+ if (cheat.end_of_loop) {
+ --scope;
+ const auto idx = pairs.at(scope);
+ block_pairs.insert_or_assign(idx, i);
+ } else {
+ pairs.insert_or_assign(scope, i);
+ ++scope;
+ }
+ break;
+ }
+ }
+ }
+}
+
+void CheatList::WriteImmediate(const Cheat& cheat) {
+ const auto offset =
+ cheat.memory_type == MemoryType::MainNSO ? main_region_begin : heap_region_begin;
+ const auto& register_3 = scratch.at(cheat.register_3);
+
+ const auto addr = cheat.Address() + offset + register_3;
+ LOG_DEBUG(Common_Filesystem, "writing value={:016X} to addr={:016X}", addr,
+ cheat.Value(8, cheat.width));
+ writer(cheat.width, SanitizeAddress(addr), cheat.ValueWidth(8));
+}
+
+void CheatList::BeginConditional(const Cheat& cheat) {
+ if (EvaluateConditional(cheat)) {
+ return;
+ }
+
+ const auto iter = block_pairs.find(current_index);
+ ASSERT(iter != block_pairs.end());
+ current_index = iter->second - 1;
+}
+
+void CheatList::EndConditional(const Cheat& cheat) {
+ LOG_DEBUG(Common_Filesystem, "Ending conditional block.");
+}
+
+void CheatList::Loop(const Cheat& cheat) {
+ if (cheat.end_of_loop.Value())
+ ASSERT(!cheat.end_of_loop.Value());
+
+ auto& register_3 = scratch.at(cheat.register_3);
+ const auto iter = block_pairs.find(current_index);
+ ASSERT(iter != block_pairs.end());
+ ASSERT(iter->first < iter->second);
+
+ for (int i = cheat.Value(4, 4); i >= 0; --i) {
+ register_3 = i;
+ for (std::size_t c = iter->first + 1; c < iter->second; ++c) {
+ current_index = c;
+ ExecuteSingleCheat(
+ (in_standard ? standard_list : master_list)[current_block].second[c]);
+ }
+ }
+
+ current_index = iter->second;
+}
+
+void CheatList::LoadImmediate(const Cheat& cheat) {
+ auto& register_3 = scratch.at(cheat.register_3);
+
+ LOG_DEBUG(Common_Filesystem, "setting register={:01X} equal to value={:016X}", cheat.register_3,
+ cheat.Value(4, 8));
+ register_3 = cheat.Value(4, 8);
+}
+
+void CheatList::LoadIndexed(const Cheat& cheat) {
+ const auto offset =
+ cheat.memory_type == MemoryType::MainNSO ? main_region_begin : heap_region_begin;
+ auto& register_3 = scratch.at(cheat.register_3);
+
+ const auto addr = (cheat.load_from_register.Value() ? register_3 : offset) + cheat.Address();
+ LOG_DEBUG(Common_Filesystem, "writing indexed value to register={:01X}, addr={:016X}",
+ cheat.register_3, addr);
+ register_3 = reader(cheat.width, SanitizeAddress(addr));
+}
+
+void CheatList::StoreIndexed(const Cheat& cheat) {
+ const auto& register_3 = scratch.at(cheat.register_3);
+
+ const auto addr =
+ register_3 + (cheat.add_additional_register.Value() ? scratch.at(cheat.register_6) : 0);
+ LOG_DEBUG(Common_Filesystem, "writing value={:016X} to addr={:016X}",
+ cheat.Value(4, cheat.width), addr);
+ writer(cheat.width, SanitizeAddress(addr), cheat.ValueWidth(4));
+}
+
+void CheatList::RegisterArithmetic(const Cheat& cheat) {
+ using ArithmeticFunction = u64 (*)(u64, u64);
+ constexpr std::array<ArithmeticFunction, 5> arithmetic_functions{
+ [](u64 a, u64 b) { return a + b; }, [](u64 a, u64 b) { return a - b; },
+ [](u64 a, u64 b) { return a * b; }, [](u64 a, u64 b) { return a << b; },
+ [](u64 a, u64 b) { return a >> b; },
+ };
+
+ using ArithmeticOverflowCheck = bool (*)(u64, u64);
+ constexpr std::array<ArithmeticOverflowCheck, 5> arithmetic_overflow_checks{
+ [](u64 a, u64 b) { return a > (std::numeric_limits<u64>::max() - b); }, // a + b
+ [](u64 a, u64 b) { return a > (std::numeric_limits<u64>::max() + b); }, // a - b
+ [](u64 a, u64 b) { return a > (std::numeric_limits<u64>::max() / b); }, // a * b
+ [](u64 a, u64 b) { return b >= 64 || (a & ~((1ull << (64 - b)) - 1)) != 0; }, // a << b
+ [](u64 a, u64 b) { return b >= 64 || (a & ((1ull << b) - 1)) != 0; }, // a >> b
+ };
+
+ static_assert(sizeof(arithmetic_functions) == sizeof(arithmetic_overflow_checks),
+ "Missing or have extra arithmetic overflow checks compared to functions!");
+
+ auto& register_3 = scratch.at(cheat.register_3);
+
+ ASSERT(static_cast<u8>(cheat.arithmetic_op.Value()) < 5);
+ auto* function = arithmetic_functions[static_cast<u8>(cheat.arithmetic_op.Value())];
+ auto* overflow_function =
+ arithmetic_overflow_checks[static_cast<u8>(cheat.arithmetic_op.Value())];
+ LOG_DEBUG(Common_Filesystem, "performing arithmetic with register={:01X}, value={:016X}",
+ cheat.register_3, cheat.ValueWidth(4));
+
+ if (overflow_function(register_3, cheat.ValueWidth(4))) {
+ LOG_WARNING(Common_Filesystem,
+ "overflow will occur when performing arithmetic operation={:02X} with operands "
+ "a={:016X}, b={:016X}!",
+ static_cast<u8>(cheat.arithmetic_op.Value()), register_3, cheat.ValueWidth(4));
+ }
+
+ register_3 = function(register_3, cheat.ValueWidth(4));
+}
+
+void CheatList::BeginConditionalInput(const Cheat& cheat) {
+ if (EvaluateConditional(cheat))
+ return;
+
+ const auto iter = block_pairs.find(current_index);
+ ASSERT(iter != block_pairs.end());
+ current_index = iter->second - 1;
+}
+
+VAddr CheatList::SanitizeAddress(VAddr in) const {
+ if ((in < main_region_begin || in >= main_region_end) &&
+ (in < heap_region_begin || in >= heap_region_end)) {
+ LOG_ERROR(Common_Filesystem,
+ "Cheat attempting to access memory at invalid address={:016X}, if this persists, "
+ "the cheat may be incorrect. However, this may be normal early in execution if "
+ "the game has not properly set up yet.",
+ in);
+ return 0; ///< Invalid addresses will hard crash
+ }
+
+ return in;
+}
+
+void CheatList::ExecuteSingleCheat(const Cheat& cheat) {
+ using CheatOperationFunction = void (CheatList::*)(const Cheat&);
+ constexpr std::array<CheatOperationFunction, 9> cheat_operation_functions{
+ &CheatList::WriteImmediate, &CheatList::BeginConditional,
+ &CheatList::EndConditional, &CheatList::Loop,
+ &CheatList::LoadImmediate, &CheatList::LoadIndexed,
+ &CheatList::StoreIndexed, &CheatList::RegisterArithmetic,
+ &CheatList::BeginConditionalInput,
+ };
+
+ const auto index = static_cast<u8>(cheat.type.Value());
+ ASSERT(index < sizeof(cheat_operation_functions));
+ const auto op = cheat_operation_functions[index];
+ (this->*op)(cheat);
+}
+
+void CheatList::ExecuteBlock(const Block& block) {
+ encountered_loops.clear();
+
+ ProcessBlockPairs(block);
+ for (std::size_t i = 0; i < block.size(); ++i) {
+ current_index = i;
+ ExecuteSingleCheat(block[i]);
+ i = current_index;
+ }
+}
+
+CheatParser::~CheatParser() = default;
+
+CheatList CheatParser::MakeCheatList(CheatList::ProgramSegment master,
+ CheatList::ProgramSegment standard) const {
+ return {master, standard};
+}
+
+TextCheatParser::~TextCheatParser() = default;
+
+CheatList TextCheatParser::Parse(const std::vector<u8>& data) const {
+ std::stringstream ss;
+ ss.write(reinterpret_cast<const char*>(data.data()), data.size());
+
+ std::vector<std::string> lines;
+ std::string stream_line;
+ while (std::getline(ss, stream_line)) {
+ // Remove a trailing \r
+ if (!stream_line.empty() && stream_line.back() == '\r')
+ stream_line.pop_back();
+ lines.push_back(std::move(stream_line));
+ }
+
+ CheatList::ProgramSegment master_list;
+ CheatList::ProgramSegment standard_list;
+
+ for (std::size_t i = 0; i < lines.size(); ++i) {
+ auto line = lines[i];
+
+ if (!line.empty() && (line[0] == '[' || line[0] == '{')) {
+ const auto master = line[0] == '{';
+ const auto begin = master ? line.find('{') : line.find('[');
+ const auto end = master ? line.rfind('}') : line.rfind(']');
+
+ ASSERT(begin != std::string::npos && end != std::string::npos);
+
+ const std::string patch_name{line.begin() + begin + 1, line.begin() + end};
+ CheatList::Block block{};
+
+ while (i < lines.size() - 1) {
+ line = lines[++i];
+ if (!line.empty() && (line[0] == '[' || line[0] == '{')) {
+ --i;
+ break;
+ }
+
+ if (line.size() < 8)
+ continue;
+
+ Cheat out{};
+ out.raw = ParseSingleLineCheat(line);
+ block.push_back(out);
+ }
+
+ (master ? master_list : standard_list).emplace_back(patch_name, block);
+ }
+ }
+
+ return MakeCheatList(master_list, standard_list);
+}
+
+std::array<u8, 16> TextCheatParser::ParseSingleLineCheat(const std::string& line) const {
+ std::array<u8, 16> out{};
+
+ if (line.size() < 8)
+ return out;
+
+ const auto word1 = Common::HexStringToArray<sizeof(u32)>(std::string_view{line.data(), 8});
+ std::memcpy(out.data(), word1.data(), sizeof(u32));
+
+ if (line.size() < 17 || line[8] != ' ')
+ return out;
+
+ const auto word2 = Common::HexStringToArray<sizeof(u32)>(std::string_view{line.data() + 9, 8});
+ std::memcpy(out.data() + sizeof(u32), word2.data(), sizeof(u32));
+
+ if (line.size() < 26 || line[17] != ' ') {
+ // Perform shifting in case value is truncated early.
+ const auto type = static_cast<CodeType>((out[0] & 0xF0) >> 4);
+ if (type == CodeType::Loop || type == CodeType::LoadImmediate ||
+ type == CodeType::StoreIndexed || type == CodeType::RegisterArithmetic) {
+ std::memcpy(out.data() + 8, out.data() + 4, sizeof(u32));
+ std::memset(out.data() + 4, 0, sizeof(u32));
+ }
+
+ return out;
+ }
+
+ const auto word3 = Common::HexStringToArray<sizeof(u32)>(std::string_view{line.data() + 18, 8});
+ std::memcpy(out.data() + 2 * sizeof(u32), word3.data(), sizeof(u32));
+
+ if (line.size() < 35 || line[26] != ' ') {
+ // Perform shifting in case value is truncated early.
+ const auto type = static_cast<CodeType>((out[0] & 0xF0) >> 4);
+ if (type == CodeType::WriteImmediate || type == CodeType::Conditional) {
+ std::memcpy(out.data() + 12, out.data() + 8, sizeof(u32));
+ std::memset(out.data() + 8, 0, sizeof(u32));
+ }
+
+ return out;
+ }
+
+ const auto word4 = Common::HexStringToArray<sizeof(u32)>(std::string_view{line.data() + 27, 8});
+ std::memcpy(out.data() + 3 * sizeof(u32), word4.data(), sizeof(u32));
+
+ return out;
+}
+
+u64 MemoryReadImpl(u32 width, VAddr addr) {
+ switch (width) {
+ case 1:
+ return Memory::Read8(addr);
+ case 2:
+ return Memory::Read16(addr);
+ case 4:
+ return Memory::Read32(addr);
+ case 8:
+ return Memory::Read64(addr);
+ default:
+ UNREACHABLE();
+ return 0;
+ }
+}
+
+void MemoryWriteImpl(u32 width, VAddr addr, u64 value) {
+ switch (width) {
+ case 1:
+ Memory::Write8(addr, static_cast<u8>(value));
+ break;
+ case 2:
+ Memory::Write16(addr, static_cast<u16>(value));
+ break;
+ case 4:
+ Memory::Write32(addr, static_cast<u32>(value));
+ break;
+ case 8:
+ Memory::Write64(addr, value);
+ break;
+ default:
+ UNREACHABLE();
+ }
+}
+
+CheatEngine::CheatEngine(std::vector<CheatList> cheats, const std::string& build_id,
+ VAddr code_region_start, VAddr code_region_end)
+ : cheats(std::move(cheats)) {
+ auto& core_timing{Core::System::GetInstance().CoreTiming()};
+ event = core_timing.RegisterEvent(
+ "CheatEngine::FrameCallback::" + build_id,
+ [this](u64 userdata, s64 cycles_late) { FrameCallback(userdata, cycles_late); });
+ core_timing.ScheduleEvent(CHEAT_ENGINE_TICKS, event);
+
+ const auto& vm_manager = Core::System::GetInstance().CurrentProcess()->VMManager();
+ for (auto& list : this->cheats) {
+ list.SetMemoryParameters(code_region_start, vm_manager.GetHeapRegionBaseAddress(),
+ code_region_end, vm_manager.GetHeapRegionEndAddress(),
+ &MemoryWriteImpl, &MemoryReadImpl);
+ }
+}
+
+CheatEngine::~CheatEngine() {
+ auto& core_timing{Core::System::GetInstance().CoreTiming()};
+ core_timing.UnscheduleEvent(event, 0);
+}
+
+void CheatEngine::FrameCallback(u64 userdata, int cycles_late) {
+ for (auto& list : cheats)
+ list.Execute();
+
+ auto& core_timing{Core::System::GetInstance().CoreTiming()};
+ core_timing.ScheduleEvent(CHEAT_ENGINE_TICKS - cycles_late, event);
+}
+
+} // namespace FileSys
diff --git a/src/core/file_sys/cheat_engine.h b/src/core/file_sys/cheat_engine.h
new file mode 100644
index 000000000..7ed69a2c8
--- /dev/null
+++ b/src/core/file_sys/cheat_engine.h
@@ -0,0 +1,227 @@
+// Copyright 2018 yuzu emulator team
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <map>
+#include <set>
+#include <vector>
+#include <queue>
+#include "common/bit_field.h"
+#include "common/common_types.h"
+
+namespace Core::Timing {
+struct EventType;
+}
+
+namespace FileSys {
+
+enum class CodeType : u32 {
+ // 0TMR00AA AAAAAAAA YYYYYYYY YYYYYYYY
+ // Writes a T sized value Y to the address A added to the value of register R in memory domain M
+ WriteImmediate = 0,
+
+ // 1TMC00AA AAAAAAAA YYYYYYYY YYYYYYYY
+ // Compares the T sized value Y to the value at address A in memory domain M using the
+ // conditional function C. If success, continues execution. If failure, jumps to the matching
+ // EndConditional statement.
+ Conditional = 1,
+
+ // 20000000
+ // Terminates a Conditional or ConditionalInput block.
+ EndConditional = 2,
+
+ // 300R0000 VVVVVVVV
+ // Starts looping V times, storing the current count in register R.
+ // Loop block is terminated with a matching 310R0000.
+ Loop = 3,
+
+ // 400R0000 VVVVVVVV VVVVVVVV
+ // Sets the value of register R to the value V.
+ LoadImmediate = 4,
+
+ // 5TMRI0AA AAAAAAAA
+ // Sets the value of register R to the value of width T at address A in memory domain M, with
+ // the current value of R added to the address if I == 1.
+ LoadIndexed = 5,
+
+ // 6T0RIFG0 VVVVVVVV VVVVVVVV
+ // Writes the value V of width T to the memory address stored in register R. Adds the value of
+ // register G to the final calculation if F is nonzero. Increments the value of register R by T
+ // after operation if I is nonzero.
+ StoreIndexed = 6,
+
+ // 7T0RA000 VVVVVVVV
+ // Performs the arithmetic operation A on the value in register R and the value V of width T,
+ // storing the result in register R.
+ RegisterArithmetic = 7,
+
+ // 8KKKKKKK
+ // Checks to see if any of the buttons defined by the bitmask K are pressed. If any are,
+ // execution continues. If none are, execution skips to the next EndConditional command.
+ ConditionalInput = 8,
+};
+
+enum class MemoryType : u32 {
+ // Addressed relative to start of main NSO
+ MainNSO = 0,
+
+ // Addressed relative to start of heap
+ Heap = 1,
+};
+
+enum class ArithmeticOp : u32 {
+ Add = 0,
+ Sub = 1,
+ Mult = 2,
+ LShift = 3,
+ RShift = 4,
+};
+
+enum class ComparisonOp : u32 {
+ GreaterThan = 1,
+ GreaterThanEqual = 2,
+ LessThan = 3,
+ LessThanEqual = 4,
+ Equal = 5,
+ Inequal = 6,
+};
+
+union Cheat {
+ std::array<u8, 16> raw;
+
+ BitField<4, 4, CodeType> type;
+ BitField<0, 4, u32> width; // Can be 1, 2, 4, or 8. Measured in bytes.
+ BitField<0, 4, u32> end_of_loop;
+ BitField<12, 4, MemoryType> memory_type;
+ BitField<8, 4, u32> register_3;
+ BitField<8, 4, ComparisonOp> comparison_op;
+ BitField<20, 4, u32> load_from_register;
+ BitField<20, 4, u32> increment_register;
+ BitField<20, 4, ArithmeticOp> arithmetic_op;
+ BitField<16, 4, u32> add_additional_register;
+ BitField<28, 4, u32> register_6;
+
+ u64 Address() const;
+ u64 ValueWidth(u64 offset) const;
+ u64 Value(u64 offset, u64 width) const;
+ u32 KeypadValue() const;
+};
+
+class CheatParser;
+
+// Represents a full collection of cheats for a game. The Execute function should be called every
+// interval that all cheats should be executed. Clients should not directly instantiate this class
+// (hence private constructor), they should instead receive an instance from CheatParser, which
+// guarantees the list is always in an acceptable state.
+class CheatList {
+public:
+ friend class CheatParser;
+
+ using Block = std::vector<Cheat>;
+ using ProgramSegment = std::vector<std::pair<std::string, Block>>;
+
+ // (width in bytes, address, value)
+ using MemoryWriter = void (*)(u32, VAddr, u64);
+ // (width in bytes, address) -> value
+ using MemoryReader = u64 (*)(u32, VAddr);
+
+ void SetMemoryParameters(VAddr main_begin, VAddr heap_begin, VAddr main_end, VAddr heap_end,
+ MemoryWriter writer, MemoryReader reader);
+
+ void Execute();
+
+private:
+ CheatList(ProgramSegment master, ProgramSegment standard);
+
+ void ProcessBlockPairs(const Block& block);
+ void ExecuteSingleCheat(const Cheat& cheat);
+
+ void ExecuteBlock(const Block& block);
+
+ bool EvaluateConditional(const Cheat& cheat) const;
+
+ // Individual cheat operations
+ void WriteImmediate(const Cheat& cheat);
+ void BeginConditional(const Cheat& cheat);
+ void EndConditional(const Cheat& cheat);
+ void Loop(const Cheat& cheat);
+ void LoadImmediate(const Cheat& cheat);
+ void LoadIndexed(const Cheat& cheat);
+ void StoreIndexed(const Cheat& cheat);
+ void RegisterArithmetic(const Cheat& cheat);
+ void BeginConditionalInput(const Cheat& cheat);
+
+ VAddr SanitizeAddress(VAddr in) const;
+
+ // Master Codes are defined as codes that cannot be disabled and are run prior to all
+ // others.
+ ProgramSegment master_list;
+ // All other codes
+ ProgramSegment standard_list;
+
+ bool in_standard = false;
+
+ // 16 (0x0-0xF) scratch registers that can be used by cheats
+ std::array<u64, 16> scratch{};
+
+ MemoryWriter writer = nullptr;
+ MemoryReader reader = nullptr;
+
+ u64 main_region_begin{};
+ u64 heap_region_begin{};
+ u64 main_region_end{};
+ u64 heap_region_end{};
+
+ u64 current_block{};
+ // The current index of the cheat within the current Block
+ u64 current_index{};
+
+ // The 'stack' of the program. When a conditional or loop statement is encountered, its index is
+ // pushed onto this queue. When a end block is encountered, the condition is checked.
+ std::map<u64, u64> block_pairs;
+
+ std::set<u64> encountered_loops;
+};
+
+// Intermediary class that parses a text file or other disk format for storing cheats into a
+// CheatList object, that can be used for execution.
+class CheatParser {
+public:
+ virtual ~CheatParser();
+
+ virtual CheatList Parse(const std::vector<u8>& data) const = 0;
+
+protected:
+ CheatList MakeCheatList(CheatList::ProgramSegment master,
+ CheatList::ProgramSegment standard) const;
+};
+
+// CheatParser implementation that parses text files
+class TextCheatParser final : public CheatParser {
+public:
+ ~TextCheatParser() override;
+
+ CheatList Parse(const std::vector<u8>& data) const override;
+
+private:
+ std::array<u8, 16> ParseSingleLineCheat(const std::string& line) const;
+};
+
+// Class that encapsulates a CheatList and manages its interaction with memory and CoreTiming
+class CheatEngine final {
+public:
+ CheatEngine(std::vector<CheatList> cheats, const std::string& build_id, VAddr code_region_start,
+ VAddr code_region_end);
+ ~CheatEngine();
+
+private:
+ void FrameCallback(u64 userdata, int cycles_late);
+
+ Core::Timing::EventType* event;
+
+ std::vector<CheatList> cheats;
+};
+
+} // namespace FileSys
diff --git a/src/core/file_sys/patch_manager.cpp b/src/core/file_sys/patch_manager.cpp
index 61706966e..2b09e5d35 100644
--- a/src/core/file_sys/patch_manager.cpp
+++ b/src/core/file_sys/patch_manager.cpp
@@ -7,6 +7,7 @@
#include <cstddef>
#include <cstring>
+#include "common/file_util.h"
#include "common/hex_util.h"
#include "common/logging/log.h"
#include "core/file_sys/content_archive.h"
@@ -232,6 +233,57 @@ bool PatchManager::HasNSOPatch(const std::array<u8, 32>& build_id_) const {
return !CollectPatches(patch_dirs, build_id).empty();
}
+static std::optional<CheatList> ReadCheatFileFromFolder(u64 title_id,
+ const std::array<u8, 0x20>& build_id_,
+ const VirtualDir& base_path, bool upper) {
+ const auto build_id_raw = Common::HexArrayToString(build_id_, upper);
+ const auto build_id = build_id_raw.substr(0, sizeof(u64) * 2);
+ const auto file = base_path->GetFile(fmt::format("{}.txt", build_id));
+
+ if (file == nullptr) {
+ LOG_INFO(Common_Filesystem, "No cheats file found for title_id={:016X}, build_id={}",
+ title_id, build_id);
+ return std::nullopt;
+ }
+
+ std::vector<u8> data(file->GetSize());
+ if (file->Read(data.data(), data.size()) != data.size()) {
+ LOG_INFO(Common_Filesystem, "Failed to read cheats file for title_id={:016X}, build_id={}",
+ title_id, build_id);
+ return std::nullopt;
+ }
+
+ TextCheatParser parser;
+ return parser.Parse(data);
+}
+
+std::vector<CheatList> PatchManager::CreateCheatList(const std::array<u8, 32>& build_id_) const {
+ std::vector<CheatList> out;
+
+ const auto load_dir = Service::FileSystem::GetModificationLoadRoot(title_id);
+ auto patch_dirs = load_dir->GetSubdirectories();
+ std::sort(patch_dirs.begin(), patch_dirs.end(),
+ [](const VirtualDir& l, const VirtualDir& r) { return l->GetName() < r->GetName(); });
+
+ out.reserve(patch_dirs.size());
+ for (const auto& subdir : patch_dirs) {
+ auto cheats_dir = subdir->GetSubdirectory("cheats");
+ if (cheats_dir != nullptr) {
+ auto res = ReadCheatFileFromFolder(title_id, build_id_, cheats_dir, true);
+ if (res.has_value()) {
+ out.push_back(std::move(*res));
+ continue;
+ }
+
+ res = ReadCheatFileFromFolder(title_id, build_id_, cheats_dir, false);
+ if (res.has_value())
+ out.push_back(std::move(*res));
+ }
+ }
+
+ return out;
+}
+
static void ApplyLayeredFS(VirtualFile& romfs, u64 title_id, ContentRecordType type) {
const auto load_dir = Service::FileSystem::GetModificationLoadRoot(title_id);
if ((type != ContentRecordType::Program && type != ContentRecordType::Data) ||
@@ -403,6 +455,8 @@ std::map<std::string, std::string, std::less<>> PatchManager::GetPatchVersionNam
}
if (IsDirValidAndNonEmpty(mod->GetSubdirectory("romfs")))
AppendCommaIfNotEmpty(types, "LayeredFS");
+ if (IsDirValidAndNonEmpty(mod->GetSubdirectory("cheats")))
+ AppendCommaIfNotEmpty(types, "Cheats");
if (types.empty())
continue;
diff --git a/src/core/file_sys/patch_manager.h b/src/core/file_sys/patch_manager.h
index b8a1652fd..3e3ac6aca 100644
--- a/src/core/file_sys/patch_manager.h
+++ b/src/core/file_sys/patch_manager.h
@@ -8,6 +8,7 @@
#include <memory>
#include <string>
#include "common/common_types.h"
+#include "core/file_sys/cheat_engine.h"
#include "core/file_sys/nca_metadata.h"
#include "core/file_sys/vfs.h"
@@ -45,6 +46,9 @@ public:
// Used to prevent expensive copies in NSO loader.
bool HasNSOPatch(const std::array<u8, 0x20>& build_id) const;
+ // Creates a CheatList object with all
+ std::vector<CheatList> CreateCheatList(const std::array<u8, 0x20>& build_id) const;
+
// Currently tracked RomFS patches:
// - Game Updates
// - LayeredFS