From f3346262d2c143c7e3b81775db5192efb0d803ff Mon Sep 17 00:00:00 2001 From: "Admiral H. Curtiss" Date: Sun, 22 Aug 2021 07:12:51 +0200 Subject: [PATCH 1/2] Core/CheatSearch: Implement cheat searching functionality. --- Source/Core/Core/CMakeLists.txt | 4 + Source/Core/Core/CheatGeneration.cpp | 77 ++++ Source/Core/Core/CheatGeneration.h | 23 + Source/Core/Core/CheatSearch.cpp | 640 +++++++++++++++++++++++++++ Source/Core/Core/CheatSearch.h | 219 +++++++++ Source/Core/DolphinLib.props | 4 + 6 files changed, 967 insertions(+) create mode 100644 Source/Core/Core/CheatGeneration.cpp create mode 100644 Source/Core/Core/CheatGeneration.h create mode 100644 Source/Core/Core/CheatSearch.cpp create mode 100644 Source/Core/Core/CheatSearch.h diff --git a/Source/Core/Core/CMakeLists.txt b/Source/Core/Core/CMakeLists.txt index 7f6e6cc838..b22f523811 100644 --- a/Source/Core/Core/CMakeLists.txt +++ b/Source/Core/Core/CMakeLists.txt @@ -15,6 +15,10 @@ add_library(core BootManager.cpp BootManager.h CheatCodes.h + CheatGeneration.cpp + CheatGeneration.h + CheatSearch.cpp + CheatSearch.h CommonTitles.h Config/DefaultLocale.cpp Config/DefaultLocale.h diff --git a/Source/Core/Core/CheatGeneration.cpp b/Source/Core/Core/CheatGeneration.cpp new file mode 100644 index 0000000000..8011a879ad --- /dev/null +++ b/Source/Core/Core/CheatGeneration.cpp @@ -0,0 +1,77 @@ +// Copyright 2021 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +#include + +#include "Core/CheatGeneration.h" + +#include "Common/Align.h" +#include "Common/CommonTypes.h" +#include "Common/Result.h" +#include "Common/Swap.h" + +#include "Core/ActionReplay.h" +#include "Core/CheatSearch.h" + +constexpr int AR_SET_BYTE_CMD = 0x00; +constexpr int AR_SET_SHORT_CMD = 0x02; +constexpr int AR_SET_INT_CMD = 0x04; + +static std::vector ResultToAREntries(u32 addr, const Cheats::SearchValue& sv) +{ + std::vector codes; + std::vector data = Cheats::GetValueAsByteVector(sv); + + for (size_t i = 0; i < data.size(); ++i) + { + const u32 address = (addr + i) & 0x01ff'ffffu; + if (Common::AlignUp(address, 4) == address && i + 3 < data.size()) + { + const u8 cmd = AR_SET_INT_CMD; + const u32 val = Common::swap32(&data[i]); + codes.emplace_back((cmd << 24) | address, val); + i += 3; + } + else if (Common::AlignUp(address, 2) == address && i + 1 < data.size()) + { + const u8 cmd = AR_SET_SHORT_CMD; + const u32 val = Common::swap16(&data[i]); + codes.emplace_back((cmd << 24) | address, val); + ++i; + } + else + { + const u8 cmd = AR_SET_BYTE_CMD; + const u32 val = data[i]; + codes.emplace_back((cmd << 24) | address, val); + } + } + + return codes; +} + +Common::Result +Cheats::GenerateActionReplayCode(const Cheats::CheatSearchSessionBase& session, size_t index) +{ + if (index >= session.GetResultCount()) + return Cheats::GenerateActionReplayCodeErrorCode::IndexOutOfRange; + + if (session.GetResultValueState(index) != Cheats::SearchResultValueState::ValueFromVirtualMemory) + return Cheats::GenerateActionReplayCodeErrorCode::NotVirtualMemory; + + u32 address = session.GetResultAddress(index); + + // check if the address is actually addressable by the ActionReplay system + if (((address & 0x01ff'ffffu) | 0x8000'0000u) != address) + return Cheats::GenerateActionReplayCodeErrorCode::InvalidAddress; + + ActionReplay::ARCode ar_code; + ar_code.enabled = true; + ar_code.user_defined = true; + ar_code.name = fmt::format("Generated by Cheat Search (Address 0x{:08x})", address); + ar_code.ops = ResultToAREntries(address, session.GetResultValueAsSearchValue(index)); + + return ar_code; +} diff --git a/Source/Core/Core/CheatGeneration.h b/Source/Core/Core/CheatGeneration.h new file mode 100644 index 0000000000..f6fda3a221 --- /dev/null +++ b/Source/Core/Core/CheatGeneration.h @@ -0,0 +1,23 @@ +// Copyright 2021 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "Common/Result.h" + +#include "Core/ActionReplay.h" + +namespace Cheats +{ +class CheatSearchSessionBase; + +enum class GenerateActionReplayCodeErrorCode +{ + IndexOutOfRange, + NotVirtualMemory, + InvalidAddress, +}; + +Common::Result +GenerateActionReplayCode(const Cheats::CheatSearchSessionBase& session, size_t index); +} // namespace Cheats diff --git a/Source/Core/Core/CheatSearch.cpp b/Source/Core/Core/CheatSearch.cpp new file mode 100644 index 0000000000..f83b7f8b9d --- /dev/null +++ b/Source/Core/Core/CheatSearch.cpp @@ -0,0 +1,640 @@ +// Copyright 2021 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "Core/CheatSearch.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Common/Align.h" +#include "Common/BitUtils.h" +#include "Common/StringUtil.h" + +#include "Core/Core.h" +#include "Core/HW/Memmap.h" +#include "Core/PowerPC/MMU.h" +#include "Core/PowerPC/PowerPC.h" + +Cheats::DataType Cheats::GetDataType(const Cheats::SearchValue& value) +{ + // sanity checks that our enum matches with our std::variant + using should_be_u8 = std::remove_cv_t< + std::remove_reference_t(DataType::U8)>(value.m_value))>>; + using should_be_u16 = std::remove_cv_t< + std::remove_reference_t(DataType::U16)>(value.m_value))>>; + using should_be_u32 = std::remove_cv_t< + std::remove_reference_t(DataType::U32)>(value.m_value))>>; + using should_be_u64 = std::remove_cv_t< + std::remove_reference_t(DataType::U64)>(value.m_value))>>; + using should_be_s8 = std::remove_cv_t< + std::remove_reference_t(DataType::S8)>(value.m_value))>>; + using should_be_s16 = std::remove_cv_t< + std::remove_reference_t(DataType::S16)>(value.m_value))>>; + using should_be_s32 = std::remove_cv_t< + std::remove_reference_t(DataType::S32)>(value.m_value))>>; + using should_be_s64 = std::remove_cv_t< + std::remove_reference_t(DataType::S64)>(value.m_value))>>; + using should_be_f32 = std::remove_cv_t< + std::remove_reference_t(DataType::F32)>(value.m_value))>>; + using should_be_f64 = std::remove_cv_t< + std::remove_reference_t(DataType::F64)>(value.m_value))>>; + static_assert(std::is_same_v); + static_assert(std::is_same_v); + static_assert(std::is_same_v); + static_assert(std::is_same_v); + static_assert(std::is_same_v); + static_assert(std::is_same_v); + static_assert(std::is_same_v); + static_assert(std::is_same_v); + static_assert(std::is_same_v); + static_assert(std::is_same_v); + + return static_cast(value.m_value.index()); +} + +template +static std::vector ToByteVector(const T& val) +{ + static_assert(std::is_trivially_copyable_v); + const auto* const begin = reinterpret_cast(&val); + const auto* const end = begin + sizeof(T); + return {begin, end}; +} + +std::vector Cheats::GetValueAsByteVector(const Cheats::SearchValue& value) +{ + DataType type = GetDataType(value); + switch (type) + { + case Cheats::DataType::U8: + return {std::get(value.m_value)}; + case Cheats::DataType::U16: + return ToByteVector(Common::swap16(std::get(value.m_value))); + case Cheats::DataType::U32: + return ToByteVector(Common::swap32(std::get(value.m_value))); + case Cheats::DataType::U64: + return ToByteVector(Common::swap64(std::get(value.m_value))); + case Cheats::DataType::S8: + return {Common::BitCast(std::get(value.m_value))}; + case Cheats::DataType::S16: + return ToByteVector(Common::swap16(Common::BitCast(std::get(value.m_value)))); + case Cheats::DataType::S32: + return ToByteVector(Common::swap32(Common::BitCast(std::get(value.m_value)))); + case Cheats::DataType::S64: + return ToByteVector(Common::swap64(Common::BitCast(std::get(value.m_value)))); + case Cheats::DataType::F32: + return ToByteVector(Common::swap32(Common::BitCast(std::get(value.m_value)))); + case Cheats::DataType::F64: + return ToByteVector(Common::swap64(Common::BitCast(std::get(value.m_value)))); + default: + assert(0); + return {}; + } +} + +namespace +{ +template +static PowerPC::TryReadResult +TryReadValueFromEmulatedMemory(u32 addr, PowerPC::RequestedAddressSpace space); + +template <> +PowerPC::TryReadResult TryReadValueFromEmulatedMemory(u32 addr, + PowerPC::RequestedAddressSpace space) +{ + return PowerPC::HostTryReadU8(addr, space); +} + +template <> +PowerPC::TryReadResult TryReadValueFromEmulatedMemory(u32 addr, + PowerPC::RequestedAddressSpace space) +{ + return PowerPC::HostTryReadU16(addr, space); +} + +template <> +PowerPC::TryReadResult TryReadValueFromEmulatedMemory(u32 addr, + PowerPC::RequestedAddressSpace space) +{ + return PowerPC::HostTryReadU32(addr, space); +} + +template <> +PowerPC::TryReadResult TryReadValueFromEmulatedMemory(u32 addr, + PowerPC::RequestedAddressSpace space) +{ + return PowerPC::HostTryReadU64(addr, space); +} + +template <> +PowerPC::TryReadResult TryReadValueFromEmulatedMemory(u32 addr, + PowerPC::RequestedAddressSpace space) +{ + auto tmp = PowerPC::HostTryReadU8(addr, space); + if (!tmp) + return PowerPC::TryReadResult(); + return PowerPC::TryReadResult(tmp.translated, Common::BitCast(tmp.value)); +} + +template <> +PowerPC::TryReadResult TryReadValueFromEmulatedMemory(u32 addr, + PowerPC::RequestedAddressSpace space) +{ + auto tmp = PowerPC::HostTryReadU16(addr, space); + if (!tmp) + return PowerPC::TryReadResult(); + return PowerPC::TryReadResult(tmp.translated, Common::BitCast(tmp.value)); +} + +template <> +PowerPC::TryReadResult TryReadValueFromEmulatedMemory(u32 addr, + PowerPC::RequestedAddressSpace space) +{ + auto tmp = PowerPC::HostTryReadU32(addr, space); + if (!tmp) + return PowerPC::TryReadResult(); + return PowerPC::TryReadResult(tmp.translated, Common::BitCast(tmp.value)); +} + +template <> +PowerPC::TryReadResult TryReadValueFromEmulatedMemory(u32 addr, + PowerPC::RequestedAddressSpace space) +{ + auto tmp = PowerPC::HostTryReadU64(addr, space); + if (!tmp) + return PowerPC::TryReadResult(); + return PowerPC::TryReadResult(tmp.translated, Common::BitCast(tmp.value)); +} + +template <> +PowerPC::TryReadResult TryReadValueFromEmulatedMemory(u32 addr, + PowerPC::RequestedAddressSpace space) +{ + return PowerPC::HostTryReadF32(addr, space); +} + +template <> +PowerPC::TryReadResult TryReadValueFromEmulatedMemory(u32 addr, + PowerPC::RequestedAddressSpace space) +{ + return PowerPC::HostTryReadF64(addr, space); +} +} // namespace + +template +Common::Result>> +Cheats::NewSearch(const std::vector& memory_ranges, + PowerPC::RequestedAddressSpace address_space, bool aligned, + const std::function& validator) +{ + const u32 data_size = sizeof(T); + std::vector> results; + Cheats::SearchErrorCode error_code = Cheats::SearchErrorCode::Success; + Core::RunAsCPUThread([&] { + const Core::State core_state = Core::GetState(); + if (core_state != Core::State::Running && core_state != Core::State::Paused) + { + error_code = Cheats::SearchErrorCode::NoEmulationActive; + return; + } + + if (address_space == PowerPC::RequestedAddressSpace::Virtual && !MSR.DR) + { + error_code = Cheats::SearchErrorCode::VirtualAddressesCurrentlyNotAccessible; + return; + } + + for (const Cheats::MemoryRange& range : memory_ranges) + { + const u32 increment_per_loop = aligned ? data_size : 1; + const u32 start_address = aligned ? Common::AlignUp(range.m_start, data_size) : range.m_start; + const u64 aligned_length = range.m_length - (start_address - range.m_start); + const u64 length = aligned_length - (data_size - 1); + for (u64 i = 0; i < length; i += increment_per_loop) + { + const u32 addr = start_address + i; + const auto current_value = TryReadValueFromEmulatedMemory(addr, address_space); + if (!current_value) + continue; + + if (validator(current_value.value)) + { + auto& r = results.emplace_back(); + r.m_value = current_value.value; + r.m_value_state = current_value.translated ? + Cheats::SearchResultValueState::ValueFromVirtualMemory : + Cheats::SearchResultValueState::ValueFromPhysicalMemory; + r.m_address = addr; + } + } + } + }); + if (error_code == Cheats::SearchErrorCode::Success) + return results; + return error_code; +} + +template +Common::Result>> +Cheats::NextSearch(const std::vector>& previous_results, + PowerPC::RequestedAddressSpace address_space, + const std::function& validator) +{ + std::vector> results; + Cheats::SearchErrorCode error_code = Cheats::SearchErrorCode::Success; + Core::RunAsCPUThread([&] { + const Core::State core_state = Core::GetState(); + if (core_state != Core::State::Running && core_state != Core::State::Paused) + { + error_code = Cheats::SearchErrorCode::NoEmulationActive; + return; + } + + if (address_space == PowerPC::RequestedAddressSpace::Virtual && !MSR.DR) + { + error_code = Cheats::SearchErrorCode::VirtualAddressesCurrentlyNotAccessible; + return; + } + + for (const auto& previous_result : previous_results) + { + const u32 addr = previous_result.m_address; + const auto current_value = TryReadValueFromEmulatedMemory(addr, address_space); + if (!current_value) + { + auto& r = results.emplace_back(); + r.m_address = addr; + r.m_value_state = Cheats::SearchResultValueState::AddressNotAccessible; + continue; + } + + // if the previous state was invalid we always update the value to avoid getting stuck in an + // invalid state + if (!previous_result.IsValueValid() || + validator(current_value.value, previous_result.m_value)) + { + auto& r = results.emplace_back(); + r.m_value = current_value.value; + r.m_value_state = current_value.translated ? + Cheats::SearchResultValueState::ValueFromVirtualMemory : + Cheats::SearchResultValueState::ValueFromPhysicalMemory; + r.m_address = addr; + } + } + }); + if (error_code == Cheats::SearchErrorCode::Success) + return results; + return error_code; +} + +Cheats::CheatSearchSessionBase::~CheatSearchSessionBase() = default; + +template +Cheats::CheatSearchSession::CheatSearchSession(std::vector memory_ranges, + PowerPC::RequestedAddressSpace address_space, + bool aligned) + : m_memory_ranges(std::move(memory_ranges)), m_address_space(address_space), m_aligned(aligned) +{ +} + +template +Cheats::CheatSearchSession::CheatSearchSession(const CheatSearchSession& session) = default; + +template +Cheats::CheatSearchSession::CheatSearchSession(CheatSearchSession&& session) = default; + +template +Cheats::CheatSearchSession& +Cheats::CheatSearchSession::operator=(const CheatSearchSession& session) = default; + +template +Cheats::CheatSearchSession& +Cheats::CheatSearchSession::operator=(CheatSearchSession&& session) = default; + +template +Cheats::CheatSearchSession::~CheatSearchSession() = default; + +template +void Cheats::CheatSearchSession::SetCompareType(CompareType compare_type) +{ + m_compare_type = compare_type; +} + +template +void Cheats::CheatSearchSession::SetFilterType(FilterType filter_type) +{ + m_filter_type = filter_type; +} + +template +static std::optional ParseValue(const std::string& str) +{ + if (str.empty()) + return std::nullopt; + + T tmp; + if (TryParse(str, &tmp)) + return tmp; + + return std::nullopt; +} + +template +bool Cheats::CheatSearchSession::SetValueFromString(const std::string& value_as_string) +{ + m_value = ParseValue(value_as_string); + return m_value.has_value(); +} + +template +void Cheats::CheatSearchSession::ResetResults() +{ + m_first_search_done = false; + m_search_results.clear(); +} + +template +static std::function +MakeCompareFunctionForSpecificValue(Cheats::CompareType op, const T& old_value) +{ + switch (op) + { + case Cheats::CompareType::Equal: + return [&](const T& new_value) { return new_value == old_value; }; + case Cheats::CompareType::NotEqual: + return [&](const T& new_value) { return new_value != old_value; }; + case Cheats::CompareType::Less: + return [&](const T& new_value) { return new_value < old_value; }; + case Cheats::CompareType::LessOrEqual: + return [&](const T& new_value) { return new_value <= old_value; }; + case Cheats::CompareType::Greater: + return [&](const T& new_value) { return new_value > old_value; }; + case Cheats::CompareType::GreaterOrEqual: + return [&](const T& new_value) { return new_value >= old_value; }; + default: + assert(0); + return nullptr; + } +} + +template +static std::function +MakeCompareFunctionForLastValue(Cheats::CompareType op) +{ + switch (op) + { + case Cheats::CompareType::Equal: + return [](const T& new_value, const T& old_value) { return new_value == old_value; }; + case Cheats::CompareType::NotEqual: + return [](const T& new_value, const T& old_value) { return new_value != old_value; }; + case Cheats::CompareType::Less: + return [](const T& new_value, const T& old_value) { return new_value < old_value; }; + case Cheats::CompareType::LessOrEqual: + return [](const T& new_value, const T& old_value) { return new_value <= old_value; }; + case Cheats::CompareType::Greater: + return [](const T& new_value, const T& old_value) { return new_value > old_value; }; + case Cheats::CompareType::GreaterOrEqual: + return [](const T& new_value, const T& old_value) { return new_value >= old_value; }; + default: + assert(0); + return nullptr; + } +} + +template +Cheats::SearchErrorCode Cheats::CheatSearchSession::RunSearch() +{ + Common::Result>> result = + Cheats::SearchErrorCode::InvalidParameters; + if (m_filter_type == FilterType::CompareAgainstSpecificValue) + { + if (!m_value) + return Cheats::SearchErrorCode::InvalidParameters; + + auto func = MakeCompareFunctionForSpecificValue(m_compare_type, *m_value); + if (m_first_search_done) + { + result = Cheats::NextSearch( + m_search_results, m_address_space, + [&func](const T& new_value, const T& old_value) { return func(new_value); }); + } + else + { + result = Cheats::NewSearch(m_memory_ranges, m_address_space, m_aligned, func); + } + } + else if (m_filter_type == FilterType::CompareAgainstLastValue) + { + if (!m_first_search_done) + return Cheats::SearchErrorCode::InvalidParameters; + + result = Cheats::NextSearch(m_search_results, m_address_space, + MakeCompareFunctionForLastValue(m_compare_type)); + } + else if (m_filter_type == FilterType::DoNotFilter) + { + if (m_first_search_done) + { + result = Cheats::NextSearch(m_search_results, m_address_space, + [](const T& v1, const T& v2) { return true; }); + } + else + { + result = Cheats::NewSearch(m_memory_ranges, m_address_space, m_aligned, + [](const T& v) { return true; }); + } + } + + if (result.Succeeded()) + { + m_search_results = std::move(*result); + m_first_search_done = true; + return Cheats::SearchErrorCode::Success; + } + + return result.Error(); +} + +template +size_t Cheats::CheatSearchSession::GetMemoryRangeCount() +{ + return m_memory_ranges.size(); +} + +template +Cheats::MemoryRange Cheats::CheatSearchSession::GetMemoryRange(size_t index) +{ + return m_memory_ranges[index]; +} + +template +PowerPC::RequestedAddressSpace Cheats::CheatSearchSession::GetAddressSpace() +{ + return m_address_space; +} + +template +Cheats::DataType Cheats::CheatSearchSession::GetDataType() +{ + return Cheats::GetDataType(Cheats::SearchValue{T(0)}); +} + +template +bool Cheats::CheatSearchSession::GetAligned() +{ + return m_aligned; +} + +template +size_t Cheats::CheatSearchSession::GetResultCount() const +{ + return m_search_results.size(); +} + +template +size_t Cheats::CheatSearchSession::GetValidValueCount() const +{ + const auto& results = m_search_results; + size_t count = 0; + for (const auto& r : results) + { + if (r.IsValueValid()) + ++count; + } + return count; +} + +template +u32 Cheats::CheatSearchSession::GetResultAddress(size_t index) const +{ + return m_search_results[index].m_address; +} + +template +T Cheats::CheatSearchSession::GetResultValue(size_t index) const +{ + return m_search_results[index].m_value; +} + +template +Cheats::SearchValue Cheats::CheatSearchSession::GetResultValueAsSearchValue(size_t index) const +{ + return Cheats::SearchValue{m_search_results[index].m_value}; +} + +template +std::string Cheats::CheatSearchSession::GetResultValueAsString(size_t index, bool hex) const +{ + if (GetResultValueState(index) == Cheats::SearchResultValueState::AddressNotAccessible) + return "(inaccessible)"; + + if (hex) + { + if constexpr (std::is_same_v) + return fmt::format("0x{0:08x}", Common::BitCast(m_search_results[index].m_value)); + else if constexpr (std::is_same_v) + return fmt::format("0x{0:016x}", Common::BitCast(m_search_results[index].m_value)); + else + return fmt::format("0x{0:0{1}x}", m_search_results[index].m_value, sizeof(T) * 2); + } + + return fmt::format("{}", m_search_results[index].m_value); +} + +template +Cheats::SearchResultValueState +Cheats::CheatSearchSession::GetResultValueState(size_t index) const +{ + return m_search_results[index].m_value_state; +} + +template +bool Cheats::CheatSearchSession::WasFirstSearchDone() const +{ + return m_first_search_done; +} + +template +std::unique_ptr Cheats::CheatSearchSession::Clone() const +{ + return std::make_unique>(*this); +} + +template +std::unique_ptr +Cheats::CheatSearchSession::ClonePartial(const std::vector& result_indices) const +{ + const auto& results = m_search_results; + std::vector> partial_results; + partial_results.reserve(result_indices.size()); + for (size_t idx : result_indices) + partial_results.push_back(results[idx]); + + auto c = + std::make_unique>(m_memory_ranges, m_address_space, m_aligned); + c->m_search_results = std::move(partial_results); + c->m_compare_type = this->m_compare_type; + c->m_filter_type = this->m_filter_type; + c->m_value = this->m_value; + c->m_first_search_done = this->m_first_search_done; + return c; +} + +template class Cheats::CheatSearchSession; +template class Cheats::CheatSearchSession; +template class Cheats::CheatSearchSession; +template class Cheats::CheatSearchSession; +template class Cheats::CheatSearchSession; +template class Cheats::CheatSearchSession; +template class Cheats::CheatSearchSession; +template class Cheats::CheatSearchSession; +template class Cheats::CheatSearchSession; +template class Cheats::CheatSearchSession; + +std::unique_ptr +Cheats::MakeSession(std::vector memory_ranges, + PowerPC::RequestedAddressSpace address_space, bool aligned, DataType data_type) +{ + switch (data_type) + { + case Cheats::DataType::U8: + return std::make_unique>(std::move(memory_ranges), address_space, + aligned); + case Cheats::DataType::U16: + return std::make_unique>(std::move(memory_ranges), address_space, + aligned); + case Cheats::DataType::U32: + return std::make_unique>(std::move(memory_ranges), address_space, + aligned); + case Cheats::DataType::U64: + return std::make_unique>(std::move(memory_ranges), address_space, + aligned); + case Cheats::DataType::S8: + return std::make_unique>(std::move(memory_ranges), address_space, + aligned); + case Cheats::DataType::S16: + return std::make_unique>(std::move(memory_ranges), address_space, + aligned); + case Cheats::DataType::S32: + return std::make_unique>(std::move(memory_ranges), address_space, + aligned); + case Cheats::DataType::S64: + return std::make_unique>(std::move(memory_ranges), address_space, + aligned); + case Cheats::DataType::F32: + return std::make_unique>(std::move(memory_ranges), address_space, + aligned); + case Cheats::DataType::F64: + return std::make_unique>(std::move(memory_ranges), address_space, + aligned); + default: + assert(0); + return nullptr; + } +} diff --git a/Source/Core/Core/CheatSearch.h b/Source/Core/Core/CheatSearch.h new file mode 100644 index 0000000000..5b777f50f8 --- /dev/null +++ b/Source/Core/Core/CheatSearch.h @@ -0,0 +1,219 @@ +// Copyright 2021 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "Common/CommonTypes.h" +#include "Common/Result.h" +#include "Core/PowerPC/MMU.h" + +namespace Cheats +{ +enum class CompareType +{ + Equal, + NotEqual, + Less, + LessOrEqual, + Greater, + GreaterOrEqual, +}; + +enum class FilterType +{ + CompareAgainstSpecificValue, + CompareAgainstLastValue, + DoNotFilter, +}; + +enum class DataType +{ + U8, + U16, + U32, + U64, + S8, + S16, + S32, + S64, + F32, + F64, +}; + +struct SearchValue +{ + std::variant m_value; +}; + +enum class SearchResultValueState : u8 +{ + ValueFromPhysicalMemory, + ValueFromVirtualMemory, + AddressNotAccessible, +}; + +template +struct SearchResult +{ + T m_value; + SearchResultValueState m_value_state; + u32 m_address; + + bool IsValueValid() const + { + return m_value_state == SearchResultValueState::ValueFromPhysicalMemory || + m_value_state == SearchResultValueState::ValueFromVirtualMemory; + } +}; + +struct MemoryRange +{ + u32 m_start; + u64 m_length; + + MemoryRange(u32 start, u64 length) : m_start(start), m_length(length) {} +}; + +enum class SearchErrorCode +{ + Success, + + // No emulation is currently active. + NoEmulationActive, + + // The parameter set given to the search function is bogus. + InvalidParameters, + + // This is returned if PowerPC::RequestedAddressSpace::Virtual is given but the MSR.DR flag is + // currently off in the emulated game. + VirtualAddressesCurrentlyNotAccessible, +}; + +// Returns the corresponding DataType enum for the value currently held by the given SearchValue. +DataType GetDataType(const SearchValue& value); + +// Converts the given value to a std::vector, regardless of the actual stored value type. +// Returned array is in big endian byte order, so it can be copied directly into emulated memory for +// patches or action replay codes. +std::vector GetValueAsByteVector(const SearchValue& value); + +// Do a new search across the given memory region in the given address space, only keeping values +// for which the given validator returns true. +template +Common::Result>> +NewSearch(const std::vector& memory_ranges, + PowerPC::RequestedAddressSpace address_space, bool aligned, + const std::function& validator); + +// Refresh the values for the given results in the given address space, only keeping values for +// which the given validator returns true. +template +Common::Result>> +NextSearch(const std::vector>& previous_results, + PowerPC::RequestedAddressSpace address_space, + const std::function& validator); + +class CheatSearchSessionBase +{ +public: + virtual ~CheatSearchSessionBase(); + + // Set the value comparison operation used by subsequent searches. + virtual void SetCompareType(CompareType compare_type) = 0; + + // Set the filtering value target used by subsequent searches. + virtual void SetFilterType(FilterType filter_type) = 0; + + // Set the value of the CompareAgainstSpecificValue filter used by subsequent searches. + virtual bool SetValueFromString(const std::string& value_as_string) = 0; + + // Resets the search results, causing the next search to act as a new search. + virtual void ResetResults() = 0; + + // Run either a new search or a next search based on the current state of this session. + virtual SearchErrorCode RunSearch() = 0; + + virtual size_t GetMemoryRangeCount() = 0; + virtual MemoryRange GetMemoryRange(size_t index) = 0; + virtual PowerPC::RequestedAddressSpace GetAddressSpace() = 0; + virtual DataType GetDataType() = 0; + virtual bool GetAligned() = 0; + + virtual size_t GetResultCount() const = 0; + virtual size_t GetValidValueCount() const = 0; + virtual u32 GetResultAddress(size_t index) const = 0; + virtual SearchValue GetResultValueAsSearchValue(size_t index) const = 0; + virtual std::string GetResultValueAsString(size_t index, bool hex) const = 0; + virtual SearchResultValueState GetResultValueState(size_t index) const = 0; + virtual bool WasFirstSearchDone() const = 0; + + // Create a complete copy of this search session. + virtual std::unique_ptr Clone() const = 0; + + // Create a partial copy of this search session. Only the results with the passed indices are + // copied. This is useful if you want to run a next search on only partial result data of a + // previous search. + virtual std::unique_ptr + ClonePartial(const std::vector& result_indices) const = 0; +}; + +template +class CheatSearchSession : public CheatSearchSessionBase +{ +public: + CheatSearchSession(std::vector memory_ranges, + PowerPC::RequestedAddressSpace address_space, bool aligned); + CheatSearchSession(const CheatSearchSession& session); + CheatSearchSession(CheatSearchSession&& session); + CheatSearchSession& operator=(const CheatSearchSession& session); + CheatSearchSession& operator=(CheatSearchSession&& session); + ~CheatSearchSession() override; + + void SetCompareType(CompareType compare_type) override; + void SetFilterType(FilterType filter_type) override; + bool SetValueFromString(const std::string& value_as_string) override; + + void ResetResults() override; + SearchErrorCode RunSearch() override; + + size_t GetMemoryRangeCount() override; + MemoryRange GetMemoryRange(size_t index) override; + PowerPC::RequestedAddressSpace GetAddressSpace() override; + DataType GetDataType() override; + bool GetAligned() override; + + size_t GetResultCount() const override; + size_t GetValidValueCount() const override; + u32 GetResultAddress(size_t index) const override; + T GetResultValue(size_t index) const; + SearchValue GetResultValueAsSearchValue(size_t index) const override; + std::string GetResultValueAsString(size_t index, bool hex) const override; + SearchResultValueState GetResultValueState(size_t index) const override; + bool WasFirstSearchDone() const override; + + std::unique_ptr Clone() const override; + std::unique_ptr + ClonePartial(const std::vector& result_indices) const override; + +private: + std::vector> m_search_results; + std::vector m_memory_ranges; + PowerPC::RequestedAddressSpace m_address_space; + CompareType m_compare_type = CompareType::Equal; + FilterType m_filter_type = FilterType::DoNotFilter; + std::optional m_value = std::nullopt; + bool m_aligned; + bool m_first_search_done = false; +}; + +std::unique_ptr MakeSession(std::vector memory_ranges, + PowerPC::RequestedAddressSpace address_space, + bool aligned, DataType data_type); +} // namespace Cheats diff --git a/Source/Core/DolphinLib.props b/Source/Core/DolphinLib.props index 02c58f2193..6b83842839 100644 --- a/Source/Core/DolphinLib.props +++ b/Source/Core/DolphinLib.props @@ -163,6 +163,8 @@ + + @@ -743,6 +745,8 @@ + + From 55397b6d5273c54da5ebdf1fa3790a593506fb23 Mon Sep 17 00:00:00 2001 From: "Admiral H. Curtiss" Date: Sun, 22 Aug 2021 07:13:00 +0200 Subject: [PATCH 2/2] DolphinQt: Rewrite cheat search GUI. --- Source/Core/DolphinQt/CMakeLists.txt | 6 + .../DolphinQt/CheatSearchFactoryWidget.cpp | 187 +++++ .../Core/DolphinQt/CheatSearchFactoryWidget.h | 51 ++ Source/Core/DolphinQt/CheatSearchWidget.cpp | 529 +++++++++++++ Source/Core/DolphinQt/CheatSearchWidget.h | 80 ++ Source/Core/DolphinQt/CheatsManager.cpp | 706 ++---------------- Source/Core/DolphinQt/CheatsManager.h | 62 +- Source/Core/DolphinQt/DolphinQt.vcxproj | 6 + .../QtUtils/PartiallyClosableTabWidget.cpp | 19 + .../QtUtils/PartiallyClosableTabWidget.h | 17 + 10 files changed, 955 insertions(+), 708 deletions(-) create mode 100644 Source/Core/DolphinQt/CheatSearchFactoryWidget.cpp create mode 100644 Source/Core/DolphinQt/CheatSearchFactoryWidget.h create mode 100644 Source/Core/DolphinQt/CheatSearchWidget.cpp create mode 100644 Source/Core/DolphinQt/CheatSearchWidget.h create mode 100644 Source/Core/DolphinQt/QtUtils/PartiallyClosableTabWidget.cpp create mode 100644 Source/Core/DolphinQt/QtUtils/PartiallyClosableTabWidget.h diff --git a/Source/Core/DolphinQt/CMakeLists.txt b/Source/Core/DolphinQt/CMakeLists.txt index 93b6f73257..1c8a41d606 100644 --- a/Source/Core/DolphinQt/CMakeLists.txt +++ b/Source/Core/DolphinQt/CMakeLists.txt @@ -16,6 +16,10 @@ set(CMAKE_AUTOMOC ON) add_executable(dolphin-emu AboutDialog.cpp AboutDialog.h + CheatSearchFactoryWidget.cpp + CheatSearchFactoryWidget.h + CheatSearchWidget.cpp + CheatSearchWidget.h CheatsManager.cpp CheatsManager.h ConvertDialog.cpp @@ -269,6 +273,8 @@ add_executable(dolphin-emu QtUtils/ModalMessageBox.cpp QtUtils/ModalMessageBox.h QtUtils/ParallelProgressDialog.h + QtUtils/PartiallyClosableTabWidget.cpp + QtUtils/PartiallyClosableTabWidget.h QtUtils/ImageConverter.cpp QtUtils/ImageConverter.h QtUtils/UTF8CodePointCountValidator.cpp diff --git a/Source/Core/DolphinQt/CheatSearchFactoryWidget.cpp b/Source/Core/DolphinQt/CheatSearchFactoryWidget.cpp new file mode 100644 index 0000000000..4e0f8f2f1b --- /dev/null +++ b/Source/Core/DolphinQt/CheatSearchFactoryWidget.cpp @@ -0,0 +1,187 @@ +// Copyright 2021 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "DolphinQt/CheatSearchFactoryWidget.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Common/StringUtil.h" +#include "Core/CheatSearch.h" +#include "Core/Config/MainSettings.h" +#include "Core/ConfigManager.h" +#include "Core/HW/Memmap.h" +#include "Core/PowerPC/MMU.h" + +CheatSearchFactoryWidget::CheatSearchFactoryWidget() +{ + CreateWidgets(); + ConnectWidgets(); + RefreshGui(); +} + +CheatSearchFactoryWidget::~CheatSearchFactoryWidget() = default; + +Q_DECLARE_METATYPE(Cheats::DataType); + +void CheatSearchFactoryWidget::CreateWidgets() +{ + auto* layout = new QVBoxLayout(); + + auto* address_space_group = new QGroupBox(tr("Address Space")); + auto* address_space_layout = new QVBoxLayout(); + address_space_group->setLayout(address_space_layout); + + m_standard_address_space = new QRadioButton(tr("Typical GameCube/Wii Address Space")); + m_standard_address_space->setChecked(true); + m_custom_address_space = new QRadioButton(tr("Custom Address Space")); + + QLabel* label_standard_address_space = + new QLabel(tr("Sets up the search using standard MEM1 and (on Wii) MEM2 mappings in virtual " + "address space. This will work for the vast majority of games.")); + label_standard_address_space->setWordWrap(true); + + auto* custom_address_space_layout = new QVBoxLayout(); + custom_address_space_layout->setMargin(6); + auto* custom_address_space_button_group = new QButtonGroup(); + m_custom_virtual_address_space = new QRadioButton(tr("Use virtual addresses when possible")); + m_custom_virtual_address_space->setChecked(true); + m_custom_physical_address_space = new QRadioButton(tr("Use physical addresses")); + m_custom_effective_address_space = + new QRadioButton(tr("Use memory mapper configuration at time of scan")); + custom_address_space_button_group->addButton(m_custom_virtual_address_space); + custom_address_space_button_group->addButton(m_custom_physical_address_space); + custom_address_space_button_group->addButton(m_custom_effective_address_space); + custom_address_space_layout->addWidget(m_custom_virtual_address_space); + custom_address_space_layout->addWidget(m_custom_physical_address_space); + custom_address_space_layout->addWidget(m_custom_effective_address_space); + + QLabel* label_range_start = new QLabel(tr("Range Start: ")); + m_custom_address_start = new QLineEdit(QStringLiteral("0x80000000")); + QLabel* label_range_end = new QLabel(tr("Range End: ")); + m_custom_address_end = new QLineEdit(QStringLiteral("0x81800000")); + custom_address_space_layout->addWidget(label_range_start); + custom_address_space_layout->addWidget(m_custom_address_start); + custom_address_space_layout->addWidget(label_range_end); + custom_address_space_layout->addWidget(m_custom_address_end); + + address_space_layout->addWidget(m_standard_address_space); + address_space_layout->addWidget(label_standard_address_space); + address_space_layout->addWidget(m_custom_address_space); + address_space_layout->addLayout(custom_address_space_layout); + + layout->addWidget(address_space_group); + + auto* data_type_group = new QGroupBox(tr("Data Type")); + auto* data_type_layout = new QVBoxLayout(); + data_type_group->setLayout(data_type_layout); + + m_data_type_dropdown = new QComboBox(); + m_data_type_dropdown->addItem(tr("8-bit Unsigned Integer"), + QVariant::fromValue(Cheats::DataType::U8)); + m_data_type_dropdown->addItem(tr("16-bit Unsigned Integer"), + QVariant::fromValue(Cheats::DataType::U16)); + m_data_type_dropdown->addItem(tr("32-bit Unsigned Integer"), + QVariant::fromValue(Cheats::DataType::U32)); + m_data_type_dropdown->addItem(tr("64-bit Unsigned Integer"), + QVariant::fromValue(Cheats::DataType::U64)); + m_data_type_dropdown->addItem(tr("8-bit Signed Integer"), + QVariant::fromValue(Cheats::DataType::S8)); + m_data_type_dropdown->addItem(tr("16-bit Signed Integer"), + QVariant::fromValue(Cheats::DataType::S16)); + m_data_type_dropdown->addItem(tr("32-bit Signed Integer"), + QVariant::fromValue(Cheats::DataType::S32)); + m_data_type_dropdown->addItem(tr("64-bit Signed Integer"), + QVariant::fromValue(Cheats::DataType::S64)); + m_data_type_dropdown->addItem(tr("32-bit Float"), QVariant::fromValue(Cheats::DataType::F32)); + m_data_type_dropdown->addItem(tr("64-bit Float"), QVariant::fromValue(Cheats::DataType::F64)); + m_data_type_dropdown->setCurrentIndex(6); // select 32bit signed int by default + + data_type_layout->addWidget(m_data_type_dropdown); + + m_data_type_aligned = new QCheckBox(tr("Aligned to data type length")); + m_data_type_aligned->setChecked(true); + + data_type_layout->addWidget(m_data_type_aligned); + + layout->addWidget(data_type_group); + + m_new_search = new QPushButton(tr("New Search")); + layout->addWidget(m_new_search); + + setLayout(layout); +} + +void CheatSearchFactoryWidget::ConnectWidgets() +{ + connect(m_new_search, &QPushButton::clicked, this, &CheatSearchFactoryWidget::OnNewSearchClicked); + connect(m_standard_address_space, &QPushButton::toggled, this, + &CheatSearchFactoryWidget::OnAddressSpaceRadioChanged); + connect(m_custom_address_space, &QRadioButton::toggled, this, + &CheatSearchFactoryWidget::OnAddressSpaceRadioChanged); +} + +void CheatSearchFactoryWidget::RefreshGui() +{ + bool enable_custom = m_custom_address_space->isChecked(); + m_custom_virtual_address_space->setEnabled(enable_custom); + m_custom_physical_address_space->setEnabled(enable_custom); + m_custom_effective_address_space->setEnabled(enable_custom); + m_custom_address_start->setEnabled(enable_custom); + m_custom_address_end->setEnabled(enable_custom); +} + +void CheatSearchFactoryWidget::OnAddressSpaceRadioChanged() +{ + RefreshGui(); +} + +void CheatSearchFactoryWidget::OnNewSearchClicked() +{ + std::vector memory_ranges; + PowerPC::RequestedAddressSpace address_space; + if (m_standard_address_space->isChecked()) + { + memory_ranges.emplace_back(0x80000000, Memory::GetRamSizeReal()); + if (SConfig::GetInstance().bWii) + memory_ranges.emplace_back(0x90000000, Memory::GetExRamSizeReal()); + address_space = PowerPC::RequestedAddressSpace::Virtual; + } + else + { + const std::string address_start_str = m_custom_address_start->text().toStdString(); + const std::string address_end_str = m_custom_address_end->text().toStdString(); + + u64 address_start; + u64 address_end; + if (!TryParse(address_start_str, &address_start) || !TryParse(address_end_str, &address_end)) + return; + if (address_end <= address_start || address_end > 0x1'0000'0000) + return; + + memory_ranges.emplace_back(static_cast(address_start), address_end - address_start); + + if (m_custom_virtual_address_space->isChecked()) + address_space = PowerPC::RequestedAddressSpace::Virtual; + else if (m_custom_physical_address_space->isChecked()) + address_space = PowerPC::RequestedAddressSpace::Physical; + else + address_space = PowerPC::RequestedAddressSpace::Effective; + } + + bool aligned = m_data_type_aligned->isChecked(); + auto data_type = m_data_type_dropdown->currentData().value(); + auto session = Cheats::MakeSession(std::move(memory_ranges), address_space, aligned, data_type); + if (session) + emit NewSessionCreated(*session); +} diff --git a/Source/Core/DolphinQt/CheatSearchFactoryWidget.h b/Source/Core/DolphinQt/CheatSearchFactoryWidget.h new file mode 100644 index 0000000000..576f7f97ad --- /dev/null +++ b/Source/Core/DolphinQt/CheatSearchFactoryWidget.h @@ -0,0 +1,51 @@ +// Copyright 2021 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include + +#include "Core/CheatSearch.h" + +class QCheckBox; +class QComboBox; +class QLineEdit; +class QPushButton; +class QRadioButton; + +class CheatSearchFactoryWidget : public QWidget +{ + Q_OBJECT +public: + explicit CheatSearchFactoryWidget(); + ~CheatSearchFactoryWidget() override; + +signals: + void NewSessionCreated(const Cheats::CheatSearchSessionBase& session); + +private: + void CreateWidgets(); + void ConnectWidgets(); + + void RefreshGui(); + + void OnAddressSpaceRadioChanged(); + void OnNewSearchClicked(); + + QRadioButton* m_standard_address_space; + QRadioButton* m_custom_address_space; + + QRadioButton* m_custom_virtual_address_space; + QRadioButton* m_custom_physical_address_space; + QRadioButton* m_custom_effective_address_space; + + QLineEdit* m_custom_address_start; + QLineEdit* m_custom_address_end; + + QComboBox* m_data_type_dropdown; + QCheckBox* m_data_type_aligned; + + QPushButton* m_new_search; +}; diff --git a/Source/Core/DolphinQt/CheatSearchWidget.cpp b/Source/Core/DolphinQt/CheatSearchWidget.cpp new file mode 100644 index 0000000000..54bb058ce5 --- /dev/null +++ b/Source/Core/DolphinQt/CheatSearchWidget.cpp @@ -0,0 +1,529 @@ +// Copyright 2021 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "DolphinQt/CheatSearchWidget.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "Common/Align.h" +#include "Common/BitUtils.h" +#include "Common/StringUtil.h" +#include "Common/Swap.h" + +#include "Core/ActionReplay.h" +#include "Core/CheatGeneration.h" +#include "Core/CheatSearch.h" +#include "Core/ConfigManager.h" +#include "Core/PowerPC/PowerPC.h" + +#include "DolphinQt/Config/CheatCodeEditor.h" +#include "DolphinQt/Config/CheatWarningWidget.h" + +#include "UICommon/GameFile.h" + +constexpr size_t TABLE_MAX_ROWS = 1000; + +constexpr int ADDRESS_TABLE_ADDRESS_ROLE = Qt::UserRole; +constexpr int ADDRESS_TABLE_RESULT_INDEX_ROLE = Qt::UserRole + 1; +constexpr int ADDRESS_TABLE_COLUMN_INDEX_DESCRIPTION = 0; +constexpr int ADDRESS_TABLE_COLUMN_INDEX_ADDRESS = 1; +constexpr int ADDRESS_TABLE_COLUMN_INDEX_LAST_VALUE = 2; +constexpr int ADDRESS_TABLE_COLUMN_INDEX_CURRENT_VALUE = 3; + +CheatSearchWidget::CheatSearchWidget(std::unique_ptr session) + : m_session(std::move(session)) +{ + setAttribute(Qt::WA_DeleteOnClose); + CreateWidgets(); + ConnectWidgets(); + UpdateGuiTable(); +} + +CheatSearchWidget::~CheatSearchWidget() = default; + +Q_DECLARE_METATYPE(Cheats::CompareType); +Q_DECLARE_METATYPE(Cheats::FilterType); + +void CheatSearchWidget::CreateWidgets() +{ + QLabel* session_info_label = new QLabel(); + { + QString ranges; + size_t range_count = m_session->GetMemoryRangeCount(); + switch (range_count) + { + case 1: + { + auto m = m_session->GetMemoryRange(0); + ranges = + tr("[%1, %2]") + .arg(QString::fromStdString(fmt::format("0x{0:08x}", m.m_start))) + .arg(QString::fromStdString(fmt::format("0x{0:08x}", m.m_start + m.m_length - 1))); + break; + } + case 2: + { + auto m0 = m_session->GetMemoryRange(0); + auto m1 = m_session->GetMemoryRange(1); + ranges = + tr("[%1, %2] and [%3, %4]") + .arg(QString::fromStdString(fmt::format("0x{0:08x}", m0.m_start))) + .arg(QString::fromStdString(fmt::format("0x{0:08x}", m0.m_start + m0.m_length - 1))) + .arg(QString::fromStdString(fmt::format("0x{0:08x}", m1.m_start))) + .arg(QString::fromStdString(fmt::format("0x{0:08x}", m1.m_start + m1.m_length - 1))); + break; + } + default: + ranges = tr("%1 memory ranges").arg(range_count); + break; + } + + QString space; + switch (m_session->GetAddressSpace()) + { + case PowerPC::RequestedAddressSpace::Effective: + space = tr("Address space by CPU state"); + break; + case PowerPC::RequestedAddressSpace::Physical: + space = tr("Physical address space"); + break; + case PowerPC::RequestedAddressSpace::Virtual: + space = tr("Virtual address space"); + break; + default: + space = tr("Unknown address space"); + break; + } + + QString type; + switch (m_session->GetDataType()) + { + case Cheats::DataType::U8: + type = tr("8-bit Unsigned Integer"); + break; + case Cheats::DataType::U16: + type = tr("16-bit Unsigned Integer"); + break; + case Cheats::DataType::U32: + type = tr("32-bit Unsigned Integer"); + break; + case Cheats::DataType::U64: + type = tr("64-bit Unsigned Integer"); + break; + case Cheats::DataType::S8: + type = tr("8-bit Signed Integer"); + break; + case Cheats::DataType::S16: + type = tr("16-bit Signed Integer"); + break; + case Cheats::DataType::S32: + type = tr("32-bit Signed Integer"); + break; + case Cheats::DataType::S64: + type = tr("64-bit Signed Integer"); + break; + case Cheats::DataType::F32: + type = tr("32-bit Float"); + break; + case Cheats::DataType::F64: + type = tr("64-bit Float"); + break; + default: + type = tr("Unknown data type"); + break; + } + QString aligned = m_session->GetAligned() ? tr("aligned") : tr("unaligned"); + session_info_label->setText(tr("%1, %2, %3, %4").arg(ranges).arg(space).arg(type).arg(aligned)); + } + + auto* value_layout = new QHBoxLayout(); + + auto* instructions_label = new QLabel(tr("Keep addresses where value in memory")); + value_layout->addWidget(instructions_label); + + m_compare_type_dropdown = new QComboBox(); + m_compare_type_dropdown->addItem(tr("is equal to"), + QVariant::fromValue(Cheats::CompareType::Equal)); + m_compare_type_dropdown->addItem(tr("is not equal to"), + QVariant::fromValue(Cheats::CompareType::NotEqual)); + m_compare_type_dropdown->addItem(tr("is less than"), + QVariant::fromValue(Cheats::CompareType::Less)); + m_compare_type_dropdown->addItem(tr("is less than or equal to"), + QVariant::fromValue(Cheats::CompareType::LessOrEqual)); + m_compare_type_dropdown->addItem(tr("is greater than"), + QVariant::fromValue(Cheats::CompareType::Greater)); + m_compare_type_dropdown->addItem(tr("is greater than or equal to"), + QVariant::fromValue(Cheats::CompareType::GreaterOrEqual)); + value_layout->addWidget(m_compare_type_dropdown); + + m_value_source_dropdown = new QComboBox(); + m_value_source_dropdown->addItem( + tr("this value:"), QVariant::fromValue(Cheats::FilterType::CompareAgainstSpecificValue)); + m_value_source_dropdown->addItem( + tr("last value"), QVariant::fromValue(Cheats::FilterType::CompareAgainstLastValue)); + m_value_source_dropdown->addItem(tr("any value"), + QVariant::fromValue(Cheats::FilterType::DoNotFilter)); + value_layout->addWidget(m_value_source_dropdown); + + m_given_value_text = new QLineEdit(); + value_layout->addWidget(m_given_value_text); + + auto* button_layout = new QHBoxLayout(); + m_next_scan_button = new QPushButton(tr("Search and Filter")); + button_layout->addWidget(m_next_scan_button); + m_refresh_values_button = new QPushButton(tr("Refresh Current Values")); + button_layout->addWidget(m_refresh_values_button); + m_reset_button = new QPushButton(tr("Reset Results")); + button_layout->addWidget(m_reset_button); + + m_address_table = new QTableWidget(); + m_address_table->setContextMenuPolicy(Qt::CustomContextMenu); + + m_info_label_1 = new QLabel(tr("Waiting for first scan...")); + m_info_label_2 = new QLabel(); + + m_display_values_in_hex_checkbox = new QCheckBox(tr("Display values in Hex")); + + QVBoxLayout* layout = new QVBoxLayout(); + layout->addWidget(session_info_label); + layout->addLayout(value_layout); + layout->addLayout(button_layout); + layout->addWidget(m_display_values_in_hex_checkbox); + layout->addWidget(m_info_label_1); + layout->addWidget(m_info_label_2); + layout->addWidget(m_address_table); + setLayout(layout); +} + +void CheatSearchWidget::ConnectWidgets() +{ + connect(m_next_scan_button, &QPushButton::clicked, this, &CheatSearchWidget::OnNextScanClicked); + connect(m_refresh_values_button, &QPushButton::clicked, this, + &CheatSearchWidget::OnRefreshClicked); + connect(m_reset_button, &QPushButton::clicked, this, &CheatSearchWidget::OnResetClicked); + connect(m_address_table, &QTableWidget::itemChanged, this, + &CheatSearchWidget::OnAddressTableItemChanged); + connect(m_address_table, &QTableWidget::customContextMenuRequested, this, + &CheatSearchWidget::OnAddressTableContextMenu); + connect(m_value_source_dropdown, &QComboBox::currentTextChanged, this, + &CheatSearchWidget::OnValueSourceChanged); + connect(m_display_values_in_hex_checkbox, &QCheckBox::toggled, this, + &CheatSearchWidget::OnHexCheckboxStateChanged); +} + +void CheatSearchWidget::OnNextScanClicked() +{ + const bool had_old_results = m_session->WasFirstSearchDone(); + const size_t old_count = m_session->GetResultCount(); + + const auto filter_type = m_value_source_dropdown->currentData().value(); + if (filter_type == Cheats::FilterType::CompareAgainstLastValue && + !m_session->WasFirstSearchDone()) + { + m_info_label_1->setText(tr("Cannot compare against last value on first search.")); + return; + } + m_session->SetFilterType(filter_type); + m_session->SetCompareType(m_compare_type_dropdown->currentData().value()); + if (filter_type == Cheats::FilterType::CompareAgainstSpecificValue) + { + if (!m_session->SetValueFromString(m_given_value_text->text().toStdString())) + { + m_info_label_1->setText(tr("Failed to parse given value into target data type.")); + return; + } + } + Cheats::SearchErrorCode error_code = m_session->RunSearch(); + + if (error_code == Cheats::SearchErrorCode::Success) + { + const size_t new_count = m_session->GetResultCount(); + const size_t new_valid_count = m_session->GetValidValueCount(); + m_info_label_1->setText(tr("Scan succeeded.")); + + if (had_old_results) + { + const QString removed_str = + tr("%n address(es) were removed.", "", static_cast(old_count - new_count)); + const QString remain_str = tr("%n address(es) remain.", "", static_cast(new_count)); + + if (new_valid_count == new_count) + { + m_info_label_2->setText(tr("%1 %2").arg(removed_str).arg(remain_str)); + } + else + { + const QString inaccessible_str = + tr("%n address(es) could not be accessed in emulated memory.", "", + static_cast(new_count - new_valid_count)); + + m_info_label_2->setText( + tr("%1 %2 %3").arg(removed_str).arg(remain_str).arg(inaccessible_str)); + } + } + else + { + const QString found_str = tr("Found %n address(es).", "", static_cast(new_count)); + + if (new_valid_count == new_count) + { + m_info_label_2->setText(found_str); + } + else + { + const QString inaccessible_str = + tr("%n address(es) could not be accessed in emulated memory.", "", + static_cast(new_count - new_valid_count)); + + m_info_label_2->setText(tr("%1 %2").arg(found_str).arg(inaccessible_str)); + } + } + + m_address_table_current_values.clear(); + const bool show_in_hex = m_display_values_in_hex_checkbox->isChecked(); + const bool too_many_results = new_count > TABLE_MAX_ROWS; + const size_t result_count_to_display = too_many_results ? TABLE_MAX_ROWS : new_count; + for (size_t i = 0; i < result_count_to_display; ++i) + { + m_address_table_current_values[m_session->GetResultAddress(i)] = + m_session->GetResultValueAsString(i, show_in_hex); + } + + UpdateGuiTable(); + } + else + { + switch (error_code) + { + case Cheats::SearchErrorCode::NoEmulationActive: + m_info_label_1->setText(tr("No game is running.")); + break; + case Cheats::SearchErrorCode::InvalidParameters: + m_info_label_1->setText(tr("Invalid parameters given to search.")); + break; + case Cheats::SearchErrorCode::VirtualAddressesCurrentlyNotAccessible: + m_info_label_1->setText( + tr("Search currently not possible in virtual address space. Please run " + "the game for a bit and try again.")); + break; + default: + m_info_label_1->setText(tr("Unknown error occurred.")); + break; + } + } +} + +bool CheatSearchWidget::RefreshValues() +{ + const size_t result_count = m_session->GetResultCount(); + if (result_count == 0) + { + m_info_label_1->setText(tr("Cannot refresh without results.")); + return false; + } + + const bool too_many_results = result_count > TABLE_MAX_ROWS; + std::unique_ptr tmp; + if (too_many_results) + { + std::vector value_indices; + value_indices.reserve(TABLE_MAX_ROWS); + for (size_t i = 0; i < TABLE_MAX_ROWS; ++i) + value_indices.push_back(i); + tmp = m_session->ClonePartial(value_indices); + } + else + { + tmp = m_session->Clone(); + } + + tmp->SetFilterType(Cheats::FilterType::DoNotFilter); + if (tmp->RunSearch() != Cheats::SearchErrorCode::Success) + { + m_info_label_1->setText(tr("Refresh failed. Please run the game for a bit and try again.")); + return false; + } + + m_address_table_current_values.clear(); + const bool show_in_hex = m_display_values_in_hex_checkbox->isChecked(); + const size_t result_count_to_display = tmp->GetResultCount(); + for (size_t i = 0; i < result_count_to_display; ++i) + { + m_address_table_current_values[tmp->GetResultAddress(i)] = + tmp->GetResultValueAsString(i, show_in_hex); + } + + UpdateGuiTable(); + m_info_label_1->setText(tr("Refreshed current values.")); + return true; +} + +void CheatSearchWidget::OnRefreshClicked() +{ + RefreshValues(); +} + +void CheatSearchWidget::OnResetClicked() +{ + m_session->ResetResults(); + m_address_table_current_values.clear(); + + UpdateGuiTable(); + m_info_label_1->setText(tr("Waiting for first scan...")); + m_info_label_2->clear(); +} + +void CheatSearchWidget::OnAddressTableItemChanged(QTableWidgetItem* item) +{ + const u32 address = item->data(ADDRESS_TABLE_ADDRESS_ROLE).toUInt(); + const int column = item->column(); + + switch (column) + { + case ADDRESS_TABLE_COLUMN_INDEX_DESCRIPTION: + { + m_address_table_user_data[address].m_description = item->text().toStdString(); + break; + } + default: + break; + } +} + +void CheatSearchWidget::OnAddressTableContextMenu() +{ + if (m_address_table->selectedItems().isEmpty()) + return; + + QMenu* menu = new QMenu(this); + + menu->addAction(tr("Generate Action Replay Code"), this, &CheatSearchWidget::GenerateARCode); + + menu->exec(QCursor::pos()); +} + +void CheatSearchWidget::OnValueSourceChanged() +{ + const auto filter_type = m_value_source_dropdown->currentData().value(); + m_given_value_text->setEnabled(filter_type == Cheats::FilterType::CompareAgainstSpecificValue); +} + +void CheatSearchWidget::OnHexCheckboxStateChanged() +{ + if (!m_session->WasFirstSearchDone()) + return; + + if (!RefreshValues()) + UpdateGuiTable(); +} + +void CheatSearchWidget::GenerateARCode() +{ + if (m_address_table->selectedItems().isEmpty()) + return; + + auto* item = m_address_table->selectedItems()[0]; + if (!item) + return; + + const u32 index = item->data(ADDRESS_TABLE_RESULT_INDEX_ROLE).toUInt(); + auto result = Cheats::GenerateActionReplayCode(*m_session, index); + if (result) + { + emit ActionReplayCodeGenerated(*result); + m_info_label_1->setText(tr("Generated AR code.")); + } + else + { + switch (result.Error()) + { + case Cheats::GenerateActionReplayCodeErrorCode::NotVirtualMemory: + m_info_label_1->setText(tr("Can only generate AR code for values in virtual memory.")); + break; + case Cheats::GenerateActionReplayCodeErrorCode::InvalidAddress: + m_info_label_1->setText(tr("Cannot generate AR code for this address.")); + break; + default: + m_info_label_1->setText(tr("Internal error while generating AR code.")); + break; + } + } +} + +void CheatSearchWidget::UpdateGuiTable() +{ + const QSignalBlocker blocker(m_address_table); + + m_address_table->clear(); + m_address_table->setColumnCount(4); + m_address_table->setHorizontalHeaderLabels( + {tr("Description"), tr("Address"), tr("Last Value"), tr("Current Value")}); + + const size_t result_count = m_session->GetResultCount(); + const bool too_many_results = result_count > TABLE_MAX_ROWS; + const int result_count_to_display = int(too_many_results ? TABLE_MAX_ROWS : result_count); + m_address_table->setRowCount(result_count_to_display); + + for (int i = 0; i < result_count_to_display; ++i) + { + const u32 address = m_session->GetResultAddress(i); + const auto user_data_it = m_address_table_user_data.find(address); + const bool has_user_data = user_data_it != m_address_table_user_data.end(); + + auto* description_item = new QTableWidgetItem(); + description_item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable); + if (has_user_data) + description_item->setText(QString::fromStdString(user_data_it->second.m_description)); + description_item->setData(ADDRESS_TABLE_ADDRESS_ROLE, address); + description_item->setData(ADDRESS_TABLE_RESULT_INDEX_ROLE, static_cast(i)); + m_address_table->setItem(i, ADDRESS_TABLE_COLUMN_INDEX_DESCRIPTION, description_item); + + auto* address_item = new QTableWidgetItem(); + address_item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable); + address_item->setText(QStringLiteral("0x%1").arg(address, 8, 16, QLatin1Char('0'))); + address_item->setData(ADDRESS_TABLE_ADDRESS_ROLE, address); + address_item->setData(ADDRESS_TABLE_RESULT_INDEX_ROLE, static_cast(i)); + m_address_table->setItem(i, ADDRESS_TABLE_COLUMN_INDEX_ADDRESS, address_item); + + const bool show_in_hex = m_display_values_in_hex_checkbox->isChecked(); + auto* last_value_item = new QTableWidgetItem(); + last_value_item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable); + last_value_item->setText( + QString::fromStdString(m_session->GetResultValueAsString(i, show_in_hex))); + last_value_item->setData(ADDRESS_TABLE_ADDRESS_ROLE, address); + last_value_item->setData(ADDRESS_TABLE_RESULT_INDEX_ROLE, static_cast(i)); + m_address_table->setItem(i, ADDRESS_TABLE_COLUMN_INDEX_LAST_VALUE, last_value_item); + + auto* current_value_item = new QTableWidgetItem(); + current_value_item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable); + const auto curr_val_it = m_address_table_current_values.find(address); + if (curr_val_it != m_address_table_current_values.end()) + current_value_item->setText(QString::fromStdString(curr_val_it->second)); + else + current_value_item->setText(QStringLiteral("---")); + current_value_item->setData(ADDRESS_TABLE_ADDRESS_ROLE, address); + current_value_item->setData(ADDRESS_TABLE_RESULT_INDEX_ROLE, static_cast(i)); + m_address_table->setItem(i, ADDRESS_TABLE_COLUMN_INDEX_CURRENT_VALUE, current_value_item); + } +} diff --git a/Source/Core/DolphinQt/CheatSearchWidget.h b/Source/Core/DolphinQt/CheatSearchWidget.h new file mode 100644 index 0000000000..74ee2fc2d7 --- /dev/null +++ b/Source/Core/DolphinQt/CheatSearchWidget.h @@ -0,0 +1,80 @@ +// Copyright 2021 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include +#include +#include +#include +#include + +#include "Common/CommonTypes.h" +#include "Core/CheatSearch.h" + +namespace ActionReplay +{ +struct ARCode; +} + +class QCheckBox; +class QComboBox; +class QLabel; +class QLineEdit; +class QPushButton; +class QTableWidget; +class QTableWidgetItem; + +struct CheatSearchTableUserData +{ + std::string m_description; +}; + +class CheatSearchWidget : public QWidget +{ + Q_OBJECT +public: + explicit CheatSearchWidget(std::unique_ptr session); + ~CheatSearchWidget() override; + +signals: + void ActionReplayCodeGenerated(const ActionReplay::ARCode& ar_code); + +private: + void CreateWidgets(); + void ConnectWidgets(); + + void OnNextScanClicked(); + void OnRefreshClicked(); + void OnResetClicked(); + void OnAddressTableItemChanged(QTableWidgetItem* item); + void OnAddressTableContextMenu(); + void OnValueSourceChanged(); + void OnHexCheckboxStateChanged(); + + bool RefreshValues(); + void UpdateGuiTable(); + void GenerateARCode(); + + std::unique_ptr m_session; + + // storage for the 'Current Value' column's data + std::unordered_map m_address_table_current_values; + + // storage for user-entered metadata such as text descriptions for memory addresses + // this is intentionally NOT cleared when updating values or resetting or similar + std::unordered_map m_address_table_user_data; + + QComboBox* m_compare_type_dropdown; + QComboBox* m_value_source_dropdown; + QLineEdit* m_given_value_text; + QLabel* m_info_label_1; + QLabel* m_info_label_2; + QPushButton* m_next_scan_button; + QPushButton* m_refresh_values_button; + QPushButton* m_reset_button; + QCheckBox* m_display_values_in_hex_checkbox; + QTableWidget* m_address_table; +}; diff --git a/Source/Core/DolphinQt/CheatsManager.cpp b/Source/Core/DolphinQt/CheatsManager.cpp index 7c3e51a482..ba7a9cbc1a 100644 --- a/Source/Core/DolphinQt/CheatsManager.cpp +++ b/Source/Core/DolphinQt/CheatsManager.cpp @@ -3,152 +3,24 @@ #include "DolphinQt/CheatsManager.h" -#include -#include +#include -#include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include #include #include "Core/ActionReplay.h" +#include "Core/CheatSearch.h" #include "Core/ConfigManager.h" #include "Core/Core.h" -#include "Core/Debugger/PPCDebugInterface.h" -#include "Core/HW/Memmap.h" -#include "Core/PowerPC/MMU.h" -#include "Core/PowerPC/PowerPC.h" #include "UICommon/GameFile.h" +#include "DolphinQt/CheatSearchFactoryWidget.h" +#include "DolphinQt/CheatSearchWidget.h" #include "DolphinQt/Config/ARCodeWidget.h" #include "DolphinQt/Config/GeckoCodeWidget.h" #include "DolphinQt/Settings.h" - -constexpr u32 MAX_RESULTS = 50; - -constexpr int INDEX_ROLE = Qt::UserRole; -constexpr int COLUMN_ROLE = Qt::UserRole + 1; - -constexpr int AR_SET_BYTE_CMD = 0x00; -constexpr int AR_SET_SHORT_CMD = 0x02; -constexpr int AR_SET_INT_CMD = 0x04; - -enum class CompareType : int -{ - Equal = 0, - NotEqual = 1, - Less = 2, - LessEqual = 3, - More = 4, - MoreEqual = 5 -}; - -enum class DataType : int -{ - Byte = 0, - Short = 1, - Int = 2, - Float = 3, - Double = 4, - String = 5 -}; - -struct Result -{ - u32 address; - DataType type; - QString name; - bool locked = false; - u32 locked_value; -}; - -static u32 GetResultValue(Result result) -{ - switch (result.type) - { - case DataType::Byte: - return PowerPC::HostRead_U8(result.address); - case DataType::Short: - return PowerPC::HostRead_U16(result.address); - case DataType::Int: - return PowerPC::HostRead_U32(result.address); - default: - return 0; - } -} - -static void UpdatePatch(Result result) -{ - PowerPC::debug_interface.UnsetPatch(result.address); - if (result.locked) - { - switch (result.type) - { - case DataType::Byte: - PowerPC::debug_interface.SetPatch(result.address, - std::vector{static_cast(result.locked_value)}); - break; - default: - PowerPC::debug_interface.SetPatch(result.address, result.locked_value); - break; - } - } -} - -static ActionReplay::AREntry ResultToAREntry(Result result) -{ - u8 cmd; - - switch (result.type) - { - case DataType::Byte: - cmd = AR_SET_BYTE_CMD; - break; - case DataType::Short: - cmd = AR_SET_SHORT_CMD; - break; - default: - case DataType::Int: - cmd = AR_SET_INT_CMD; - break; - } - - u32 address = result.address & 0xffffff; - - return ActionReplay::AREntry(cmd << 24 | address, result.locked_value); -} - -template -static bool Compare(T mem_value, T value, CompareType op) -{ - switch (op) - { - case CompareType::Equal: - return mem_value == value; - case CompareType::NotEqual: - return mem_value != value; - case CompareType::Less: - return mem_value < value; - case CompareType::LessEqual: - return mem_value <= value; - case CompareType::More: - return mem_value > value; - case CompareType::MoreEqual: - return mem_value >= value; - default: - return false; - } -} +#include "QtUtils/PartiallyClosableTabWidget.h" CheatsManager::CheatsManager(QWidget* parent) : QDialog(parent) { @@ -162,8 +34,6 @@ CheatsManager::CheatsManager(QWidget* parent) : QDialog(parent) CreateWidgets(); ConnectWidgets(); - Reset(); - Update(); } CheatsManager::~CheatsManager() = default; @@ -175,7 +45,7 @@ void CheatsManager::OnStateChanged(Core::State state) const auto& game_id = SConfig::GetInstance().GetGameID(); const auto& game_tdb_id = SConfig::GetInstance().GetGameTDBID(); - u16 revision = SConfig::GetInstance().GetRevision(); + const u16 revision = SConfig::GetInstance().GetRevision(); if (m_game_id == game_id && m_game_tdb_id == game_tdb_id && m_revision == revision) return; @@ -184,32 +54,40 @@ void CheatsManager::OnStateChanged(Core::State state) m_game_tdb_id = game_tdb_id; m_revision = revision; - if (m_tab_widget->count() == 3) + if (m_ar_code) { - m_tab_widget->removeTab(0); - m_tab_widget->removeTab(0); + const int tab_index = m_tab_widget->indexOf(m_ar_code); + if (tab_index != -1) + m_tab_widget->removeTab(tab_index); + m_ar_code->deleteLater(); + m_ar_code = nullptr; } - if (m_tab_widget->count() == 1) + if (m_gecko_code) { - if (m_ar_code) - m_ar_code->deleteLater(); - - m_ar_code = new ARCodeWidget(m_game_id, m_revision, false); - m_tab_widget->insertTab(0, m_ar_code, tr("AR Code")); - auto* gecko_code = new GeckoCodeWidget(m_game_id, m_game_tdb_id, m_revision, false); - m_tab_widget->insertTab(1, gecko_code, tr("Gecko Codes")); + const int tab_index = m_tab_widget->indexOf(m_gecko_code); + if (tab_index != -1) + m_tab_widget->removeTab(tab_index); + m_gecko_code->deleteLater(); + m_gecko_code = nullptr; } + + m_ar_code = new ARCodeWidget(m_game_id, m_revision, false); + m_gecko_code = new GeckoCodeWidget(m_game_id, m_game_tdb_id, m_revision, false); + m_tab_widget->insertTab(0, m_ar_code, tr("AR Code")); + m_tab_widget->insertTab(1, m_gecko_code, tr("Gecko Codes")); + m_tab_widget->setTabUnclosable(0); + m_tab_widget->setTabUnclosable(1); } void CheatsManager::CreateWidgets() { - m_tab_widget = new QTabWidget; + m_tab_widget = new PartiallyClosableTabWidget; m_button_box = new QDialogButtonBox(QDialogButtonBox::Close); - m_cheat_search = CreateCheatSearch(); - - m_tab_widget->addTab(m_cheat_search, tr("Cheat Search")); + m_cheat_search_new = new CheatSearchFactoryWidget(); + m_tab_widget->addTab(m_cheat_search_new, tr("Start New Cheat Search")); + m_tab_widget->setTabUnclosable(0); auto* layout = new QVBoxLayout; layout->addWidget(m_tab_widget); @@ -218,513 +96,27 @@ void CheatsManager::CreateWidgets() setLayout(layout); } +void CheatsManager::OnNewSessionCreated(const Cheats::CheatSearchSessionBase& session) +{ + auto* w = new CheatSearchWidget(session.Clone()); + const int tab_index = m_tab_widget->addTab(w, tr("Cheat Search")); + w->connect(w, &CheatSearchWidget::ActionReplayCodeGenerated, this, + [this](const ActionReplay::ARCode& ar_code) { + if (m_ar_code) + m_ar_code->AddCode(ar_code); + }); + m_tab_widget->setCurrentIndex(tab_index); +} + +void CheatsManager::OnTabCloseRequested(int index) +{ + m_tab_widget->removeTab(index); +} + void CheatsManager::ConnectWidgets() { connect(m_button_box, &QDialogButtonBox::rejected, this, &QDialog::reject); - - connect(m_match_new, &QPushButton::clicked, this, &CheatsManager::NewSearch); - connect(m_match_next, &QPushButton::clicked, this, &CheatsManager::NextSearch); - connect(m_match_refresh, &QPushButton::clicked, this, &CheatsManager::Update); - connect(m_match_reset, &QPushButton::clicked, this, &CheatsManager::Reset); - - m_match_table->setContextMenuPolicy(Qt::CustomContextMenu); - m_watch_table->setContextMenuPolicy(Qt::CustomContextMenu); - - connect(m_match_table, &QTableWidget::customContextMenuRequested, this, - &CheatsManager::OnMatchContextMenu); - connect(m_watch_table, &QTableWidget::customContextMenuRequested, this, - &CheatsManager::OnWatchContextMenu); - connect(m_watch_table, &QTableWidget::itemChanged, this, &CheatsManager::OnWatchItemChanged); -} - -void CheatsManager::OnWatchContextMenu() -{ - if (m_watch_table->selectedItems().isEmpty()) - return; - - QMenu* menu = new QMenu(this); - - menu->addAction(tr("Remove from Watch"), this, [this] { - auto* item = m_watch_table->selectedItems()[0]; - - int index = item->data(INDEX_ROLE).toInt(); - - m_watch.erase(m_watch.begin() + index); - - Update(); - }); - - menu->addSeparator(); - - menu->addAction(tr("Generate Action Replay Code"), this, &CheatsManager::GenerateARCode); - - menu->exec(QCursor::pos()); -} - -void CheatsManager::OnMatchContextMenu() -{ - if (m_match_table->selectedItems().isEmpty()) - return; - - QMenu* menu = new QMenu(this); - - menu->addAction(tr("Add to Watch"), this, [this] { - auto* item = m_match_table->selectedItems()[0]; - - int index = item->data(INDEX_ROLE).toInt(); - - m_results[index].locked_value = GetResultValue(m_results[index]); - - m_watch.push_back(m_results[index]); - - Update(); - }); - - menu->exec(QCursor::pos()); -} - -void CheatsManager::GenerateARCode() -{ - if (!m_ar_code) - return; - - auto* item = m_watch_table->selectedItems()[0]; - - int index = item->data(INDEX_ROLE).toInt(); - ActionReplay::ARCode ar_code; - - ar_code.enabled = true; - ar_code.user_defined = true; - ar_code.name = tr("Generated by search (Address %1)") - .arg(m_watch[index].address, 8, 16, QLatin1Char('0')) - .toStdString(); - - ar_code.ops.push_back(ResultToAREntry(m_watch[index])); - - m_ar_code->AddCode(ar_code); -} - -void CheatsManager::OnWatchItemChanged(QTableWidgetItem* item) -{ - if (m_updating) - return; - - int index = item->data(INDEX_ROLE).toInt(); - int column = item->data(COLUMN_ROLE).toInt(); - - switch (column) - { - case 0: - m_watch[index].name = item->text(); - break; - case 2: - m_watch[index].locked = item->checkState() == Qt::Checked; - - if (m_watch[index].locked) - m_watch[index].locked_value = GetResultValue(m_results[index]); - - UpdatePatch(m_watch[index]); - break; - case 3: - { - const auto text = item->text(); - u32 value = 0; - - switch (m_watch[index].type) - { - case DataType::Byte: - value = text.toUShort(nullptr, 16) & 0xFF; - break; - case DataType::Short: - value = text.toUShort(nullptr, 16); - break; - case DataType::Int: - value = text.toUInt(nullptr, 16); - break; - case DataType::Float: - { - float f = text.toFloat(); - std::memcpy(&value, &f, sizeof(float)); - break; - } - default: - break; - } - - m_watch[index].locked_value = value; - - UpdatePatch(m_watch[index]); - - break; - } - } - - Update(); -} - -QWidget* CheatsManager::CreateCheatSearch() -{ - m_match_table = new QTableWidget; - m_watch_table = new QTableWidget; - - m_match_table->setTabKeyNavigation(false); - m_watch_table->setTabKeyNavigation(false); - - m_match_table->verticalHeader()->hide(); - m_watch_table->verticalHeader()->hide(); - - m_match_table->setSelectionBehavior(QAbstractItemView::SelectRows); - m_watch_table->setSelectionBehavior(QAbstractItemView::SelectRows); - - // Options - m_result_label = new QLabel; - m_match_length = new QComboBox; - m_match_operation = new QComboBox; - m_match_value = new QLineEdit; - m_match_new = new QPushButton(tr("New Search")); - m_match_next = new QPushButton(tr("Next Search")); - m_match_refresh = new QPushButton(tr("Refresh")); - m_match_reset = new QPushButton(tr("Reset")); - - auto* options = new QWidget; - auto* layout = new QVBoxLayout; - options->setLayout(layout); - - for (const auto& option : {tr("8-bit Integer"), tr("16-bit Integer"), tr("32-bit Integer"), - tr("Float"), tr("Double"), tr("String")}) - { - m_match_length->addItem(option); - } - - for (const auto& option : {tr("Equals to"), tr("Not equals to"), tr("Less than"), - tr("Less or equal to"), tr("More than"), tr("More or equal to")}) - { - m_match_operation->addItem(option); - } - - auto* group_box = new QGroupBox(tr("Type")); - auto* group_layout = new QHBoxLayout; - group_box->setLayout(group_layout); - - // i18n: The base 10 numeral system. Not related to non-integer numbers - m_match_decimal = new QRadioButton(tr("Decimal")); - m_match_hexadecimal = new QRadioButton(tr("Hexadecimal")); - m_match_octal = new QRadioButton(tr("Octal")); - - group_layout->addWidget(m_match_decimal); - group_layout->addWidget(m_match_hexadecimal); - group_layout->addWidget(m_match_octal); - - layout->addWidget(m_result_label); - layout->addWidget(m_match_length); - layout->addWidget(m_match_operation); - layout->addWidget(m_match_value); - layout->addWidget(group_box); - layout->addWidget(m_match_new); - layout->addWidget(m_match_next); - layout->addWidget(m_match_refresh); - layout->addWidget(m_match_reset); - - // Splitters - m_option_splitter = new QSplitter(Qt::Horizontal); - m_table_splitter = new QSplitter(Qt::Vertical); - - m_table_splitter->addWidget(m_match_table); - m_table_splitter->addWidget(m_watch_table); - - m_option_splitter->addWidget(m_table_splitter); - m_option_splitter->addWidget(options); - - return m_option_splitter; -} - -size_t CheatsManager::GetTypeSize() const -{ - switch (static_cast(m_match_length->currentIndex())) - { - case DataType::Byte: - return sizeof(u8); - case DataType::Short: - return sizeof(u16); - case DataType::Int: - return sizeof(u32); - case DataType::Float: - return sizeof(float); - case DataType::Double: - return sizeof(double); - default: - return m_match_value->text().toStdString().size(); - } -} - -std::function CheatsManager::CreateMatchFunction() -{ - const QString text = m_match_value->text(); - - if (text.isEmpty()) - { - m_result_label->setText(tr("No search value entered.")); - return nullptr; - } - - const CompareType op = static_cast(m_match_operation->currentIndex()); - - const int base = - (m_match_decimal->isChecked() ? 10 : (m_match_hexadecimal->isChecked() ? 16 : 8)); - - bool conversion_succeeded = false; - std::function matches_func; - switch (static_cast(m_match_length->currentIndex())) - { - case DataType::Byte: - { - u8 comparison_value = text.toUShort(&conversion_succeeded, base) & 0xFF; - matches_func = [=](u32 addr) { - return Compare(PowerPC::HostRead_U8(addr), comparison_value, op); - }; - break; - } - case DataType::Short: - { - u16 comparison_value = text.toUShort(&conversion_succeeded, base); - matches_func = [=](u32 addr) { - return Compare(PowerPC::HostRead_U16(addr), comparison_value, op); - }; - break; - } - case DataType::Int: - { - u32 comparison_value = text.toUInt(&conversion_succeeded, base); - matches_func = [=](u32 addr) { - return Compare(PowerPC::HostRead_U32(addr), comparison_value, op); - }; - break; - } - case DataType::Float: - { - float comparison_value = text.toFloat(&conversion_succeeded); - matches_func = [=](u32 addr) { - return Compare(PowerPC::HostRead_F32(addr), comparison_value, op); - }; - break; - } - case DataType::Double: - { - double comparison_value = text.toDouble(&conversion_succeeded); - matches_func = [=](u32 addr) { - return Compare(PowerPC::HostRead_F64(addr), comparison_value, op); - }; - break; - } - case DataType::String: - { - if (op != CompareType::Equal && op != CompareType::NotEqual) - { - m_result_label->setText(tr("String values can only be compared using equality.")); - return nullptr; - } - - conversion_succeeded = true; - - const QString lambda_text = m_match_value->text(); - const QByteArray utf8_bytes = lambda_text.toUtf8(); - - matches_func = [op, utf8_bytes](u32 addr) { - bool is_equal = std::equal(utf8_bytes.cbegin(), utf8_bytes.cend(), - reinterpret_cast(Memory::m_pRAM + addr - 0x80000000)); - switch (op) - { - case CompareType::Equal: - return is_equal; - case CompareType::NotEqual: - return !is_equal; - default: - // This should never occur since we've already checked the type of op - return false; - } - }; - break; - } - } - - if (conversion_succeeded) - return matches_func; - - m_result_label->setText(tr("Cannot interpret the given value.\nHave you chosen the right type?")); - return nullptr; -} - -void CheatsManager::NewSearch() -{ - m_results.clear(); - const u32 base_address = 0x80000000; - - if (!Memory::m_pRAM) - { - m_result_label->setText(tr("Memory Not Ready")); - return; - } - - std::function matches_func = CreateMatchFunction(); - if (matches_func == nullptr) - return; - - Core::RunAsCPUThread([&] { - for (u32 i = 0; i < Memory::GetRamSizeReal() - GetTypeSize(); i++) - { - if (PowerPC::HostIsRAMAddress(base_address + i) && matches_func(base_address + i)) - m_results.push_back( - {base_address + i, static_cast(m_match_length->currentIndex())}); - } - }); - - m_match_next->setEnabled(true); - - Update(); -} - -void CheatsManager::NextSearch() -{ - if (!Memory::m_pRAM) - { - m_result_label->setText(tr("Memory Not Ready")); - return; - } - - std::function matches_func = CreateMatchFunction(); - if (matches_func == nullptr) - return; - - Core::RunAsCPUThread([this, matches_func] { - m_results.erase(std::remove_if(m_results.begin(), m_results.end(), - [matches_func](Result r) { - return !PowerPC::HostIsRAMAddress(r.address) || - !matches_func(r.address); - }), - m_results.end()); - }); - - Update(); -} - -static QString GetResultString(const Result& result) -{ - if (!PowerPC::HostIsRAMAddress(result.address)) - { - return QStringLiteral("---"); - } - switch (result.type) - { - case DataType::Byte: - return QStringLiteral("%1").arg(PowerPC::HostRead_U8(result.address), 2, 16, QLatin1Char('0')); - case DataType::Short: - return QStringLiteral("%1").arg(PowerPC::HostRead_U16(result.address), 4, 16, QLatin1Char('0')); - case DataType::Int: - return QStringLiteral("%1").arg(PowerPC::HostRead_U32(result.address), 8, 16, QLatin1Char('0')); - case DataType::Float: - return QString::number(PowerPC::HostRead_F32(result.address)); - case DataType::Double: - return QString::number(PowerPC::HostRead_F64(result.address)); - case DataType::String: - return QObject::tr("String Match"); - default: - return {}; - } -} - -void CheatsManager::Update() -{ - m_match_table->clear(); - m_watch_table->clear(); - m_match_table->setColumnCount(2); - m_watch_table->setColumnCount(4); - - m_match_table->setHorizontalHeaderLabels({tr("Address"), tr("Value")}); - m_watch_table->setHorizontalHeaderLabels({tr("Name"), tr("Address"), tr("Lock"), tr("Value")}); - - if (m_results.size() > MAX_RESULTS) - { - m_result_label->setText(tr("Too many matches to display (%1)").arg(m_results.size())); - return; - } - - m_result_label->setText(tr("%n Match(es)", "", static_cast(m_results.size()))); - m_match_table->setRowCount(static_cast(m_results.size())); - - if (m_results.empty()) - return; - - m_updating = true; - - Core::RunAsCPUThread([this] { - for (size_t i = 0; i < m_results.size(); i++) - { - auto* address_item = new QTableWidgetItem( - QStringLiteral("%1").arg(m_results[i].address, 8, 16, QLatin1Char('0'))); - auto* value_item = new QTableWidgetItem; - - address_item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable); - value_item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable); - - value_item->setText(GetResultString(m_results[i])); - - address_item->setData(INDEX_ROLE, static_cast(i)); - value_item->setData(INDEX_ROLE, static_cast(i)); - - m_match_table->setItem(static_cast(i), 0, address_item); - m_match_table->setItem(static_cast(i), 1, value_item); - } - - m_watch_table->setRowCount(static_cast(m_watch.size())); - - for (size_t i = 0; i < m_watch.size(); i++) - { - auto* name_item = new QTableWidgetItem(m_watch[i].name); - auto* address_item = new QTableWidgetItem( - QStringLiteral("%1").arg(m_watch[i].address, 8, 16, QLatin1Char('0'))); - auto* lock_item = new QTableWidgetItem; - auto* value_item = new QTableWidgetItem; - - value_item->setText(GetResultString(m_results[i])); - - name_item->setData(INDEX_ROLE, static_cast(i)); - name_item->setData(COLUMN_ROLE, 0); - - address_item->setData(INDEX_ROLE, static_cast(i)); - address_item->setData(COLUMN_ROLE, 1); - - lock_item->setData(INDEX_ROLE, static_cast(i)); - lock_item->setData(COLUMN_ROLE, 2); - - value_item->setData(INDEX_ROLE, static_cast(i)); - value_item->setData(COLUMN_ROLE, 3); - - name_item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable); - address_item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable); - lock_item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsUserCheckable | Qt::ItemIsSelectable); - value_item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable); - - lock_item->setCheckState(m_watch[i].locked ? Qt::Checked : Qt::Unchecked); - - m_watch_table->setItem(static_cast(i), 0, name_item); - m_watch_table->setItem(static_cast(i), 1, address_item); - m_watch_table->setItem(static_cast(i), 2, lock_item); - m_watch_table->setItem(static_cast(i), 3, value_item); - } - }); - - m_updating = false; -} - -void CheatsManager::Reset() -{ - m_results.clear(); - m_watch.clear(); - m_match_next->setEnabled(false); - m_match_table->clear(); - m_watch_table->clear(); - m_match_decimal->setChecked(true); - m_result_label->clear(); - - Update(); + connect(m_cheat_search_new, &CheatSearchFactoryWidget::NewSessionCreated, this, + &CheatsManager::OnNewSessionCreated); + connect(m_tab_widget, &QTabWidget::tabCloseRequested, this, &CheatsManager::OnTabCloseRequested); } diff --git a/Source/Core/DolphinQt/CheatsManager.h b/Source/Core/DolphinQt/CheatsManager.h index 6cf4179974..7cf0bb874f 100644 --- a/Source/Core/DolphinQt/CheatsManager.h +++ b/Source/Core/DolphinQt/CheatsManager.h @@ -5,6 +5,7 @@ #include #include +#include #include #include @@ -12,29 +13,19 @@ #include "Common/CommonTypes.h" #include "DolphinQt/GameList/GameListModel.h" +#include "Core/CheatSearch.h" + class ARCodeWidget; -class QComboBox; +class GeckoCodeWidget; +class CheatSearchFactoryWidget; class QDialogButtonBox; -class QLabel; -class QLineEdit; -class QPushButton; -class QRadioButton; -class QSplitter; -class QTabWidget; -class QTableWidget; -class QTableWidgetItem; -struct Result; +class PartiallyClosableTabWidget; namespace Core { enum class State; } -namespace UICommon -{ -class GameFile; -} - class CheatsManager : public QDialog { Q_OBJECT @@ -43,51 +34,20 @@ public: ~CheatsManager(); private: - QWidget* CreateCheatSearch(); void CreateWidgets(); void ConnectWidgets(); void OnStateChanged(Core::State state); - - size_t GetTypeSize() const; - std::function CreateMatchFunction(); - - void Reset(); - void NewSearch(); - void NextSearch(); - void Update(); - void GenerateARCode(); - - void OnWatchContextMenu(); - void OnMatchContextMenu(); - void OnWatchItemChanged(QTableWidgetItem* item); + void OnNewSessionCreated(const Cheats::CheatSearchSessionBase& session); + void OnTabCloseRequested(int index); std::string m_game_id; std::string m_game_tdb_id; u16 m_revision = 0; - std::vector m_results; - std::vector m_watch; QDialogButtonBox* m_button_box; - QTabWidget* m_tab_widget = nullptr; + PartiallyClosableTabWidget* m_tab_widget = nullptr; - QWidget* m_cheat_search; ARCodeWidget* m_ar_code = nullptr; - - QLabel* m_result_label; - QTableWidget* m_match_table; - QTableWidget* m_watch_table; - QSplitter* m_option_splitter; - QSplitter* m_table_splitter; - QComboBox* m_match_length; - QComboBox* m_match_operation; - QLineEdit* m_match_value; - QPushButton* m_match_new; - QPushButton* m_match_next; - QPushButton* m_match_refresh; - QPushButton* m_match_reset; - - QRadioButton* m_match_decimal; - QRadioButton* m_match_hexadecimal; - QRadioButton* m_match_octal; - bool m_updating = false; + GeckoCodeWidget* m_gecko_code = nullptr; + CheatSearchFactoryWidget* m_cheat_search_new = nullptr; }; diff --git a/Source/Core/DolphinQt/DolphinQt.vcxproj b/Source/Core/DolphinQt/DolphinQt.vcxproj index 028980080b..c8c0061bbe 100644 --- a/Source/Core/DolphinQt/DolphinQt.vcxproj +++ b/Source/Core/DolphinQt/DolphinQt.vcxproj @@ -45,6 +45,8 @@ + + @@ -168,6 +170,7 @@ + @@ -227,6 +230,8 @@ + + @@ -340,6 +345,7 @@ + diff --git a/Source/Core/DolphinQt/QtUtils/PartiallyClosableTabWidget.cpp b/Source/Core/DolphinQt/QtUtils/PartiallyClosableTabWidget.cpp new file mode 100644 index 0000000000..b8d3ef0dbd --- /dev/null +++ b/Source/Core/DolphinQt/QtUtils/PartiallyClosableTabWidget.cpp @@ -0,0 +1,19 @@ +// Copyright 2021 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "DolphinQt/QtUtils/PartiallyClosableTabWidget.h" + +#include +#include + +PartiallyClosableTabWidget::PartiallyClosableTabWidget(QWidget* parent) : QTabWidget(parent) +{ + setTabsClosable(true); +} + +void PartiallyClosableTabWidget::setTabUnclosable(int index) +{ + QTabBar::ButtonPosition closeSide = (QTabBar::ButtonPosition)style()->styleHint( + QStyle::SH_TabBar_CloseButtonPosition, nullptr, this); + tabBar()->setTabButton(index, closeSide, nullptr); +} diff --git a/Source/Core/DolphinQt/QtUtils/PartiallyClosableTabWidget.h b/Source/Core/DolphinQt/QtUtils/PartiallyClosableTabWidget.h new file mode 100644 index 0000000000..7fa1f3e160 --- /dev/null +++ b/Source/Core/DolphinQt/QtUtils/PartiallyClosableTabWidget.h @@ -0,0 +1,17 @@ +// Copyright 2021 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +class QEvent; + +class PartiallyClosableTabWidget : public QTabWidget +{ + Q_OBJECT +public: + PartiallyClosableTabWidget(QWidget* parent = nullptr); + + void setTabUnclosable(int index); +};