// Copyright 2018 yuzu emulator team // Licensed under GPLv2 or any later version // Refer to the license.txt file included. #include #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/npad.h" #include "core/hle/service/hid/hid.h" #include "core/hle/service/sm/sm.h" namespace FileSys { constexpr s64 CHEAT_ENGINE_TICKS = static_cast(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(const Core::System& system_, ProgramSegment master, ProgramSegment standard) : master_list{std::move(master)}, standard_list{std::move(standard)}, system{&system_} {} bool CheatList::EvaluateConditional(const Cheat& cheat) const { using ComparisonFunction = bool (*)(u64, u64); constexpr std::array 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 = system->ServiceManager().GetService("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::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(cheat.comparison_op.Value()) < 6); auto* function = comparison_functions[static_cast(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 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); const s32 initial_value = static_cast(cheat.Value(4, sizeof(s32))); for (s32 i = initial_value; i >= 0; --i) { register_3 = static_cast(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 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 arithmetic_overflow_checks{ [](u64 a, u64 b) { return a > (std::numeric_limits::max() - b); }, // a + b [](u64 a, u64 b) { return a > (std::numeric_limits::max() + b); }, // a - b [](u64 a, u64 b) { return a > (std::numeric_limits::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(cheat.arithmetic_op.Value()) < 5); auto* function = arithmetic_functions[static_cast(cheat.arithmetic_op.Value())]; auto* overflow_function = arithmetic_overflow_checks[static_cast(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(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 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(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(const Core::System& system, CheatList::ProgramSegment master, CheatList::ProgramSegment standard) const { return {system, std::move(master), std::move(standard)}; } TextCheatParser::~TextCheatParser() = default; CheatList TextCheatParser::Parse(const Core::System& system, const std::vector& data) const { std::stringstream ss; ss.write(reinterpret_cast(data.data()), data.size()); std::vector 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(system, master_list, standard_list); } std::array TextCheatParser::ParseSingleLineCheat(const std::string& line) const { std::array out{}; if (line.size() < 8) return out; const auto word1 = Common::HexStringToArray(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(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((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(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((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(std::string_view{line.data() + 27, 8}); std::memcpy(out.data() + 3 * sizeof(u32), word4.data(), sizeof(u32)); return out; } namespace { 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(value)); break; case 2: Memory::Write16(addr, static_cast(value)); break; case 4: Memory::Write32(addr, static_cast(value)); break; case 8: Memory::Write64(addr, value); break; default: UNREACHABLE(); } } } // Anonymous namespace CheatEngine::CheatEngine(Core::System& system, std::vector cheats_, const std::string& build_id, VAddr code_region_start, VAddr code_region_end) : cheats{std::move(cheats_)}, core_timing{system.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 = system.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() { core_timing.UnscheduleEvent(event, 0); } void CheatEngine::FrameCallback(u64 userdata, s64 cycles_late) { for (auto& list : cheats) { list.Execute(); } core_timing.ScheduleEvent(CHEAT_ENGINE_TICKS - cycles_late, event); } } // namespace FileSys