Merge pull request #9886 from AdmiralCurtiss/cheat-search
Reimplement Cheat Search.
This commit is contained in:
commit
57a8ee049f
|
@ -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
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
// Copyright 2021 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include <fmt/format.h>
|
||||
|
||||
#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<ActionReplay::AREntry> ResultToAREntries(u32 addr, const Cheats::SearchValue& sv)
|
||||
{
|
||||
std::vector<ActionReplay::AREntry> codes;
|
||||
std::vector<u8> 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::GenerateActionReplayCodeErrorCode, ActionReplay::ARCode>
|
||||
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;
|
||||
}
|
|
@ -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<GenerateActionReplayCodeErrorCode, ActionReplay::ARCode>
|
||||
GenerateActionReplayCode(const Cheats::CheatSearchSessionBase& session, size_t index);
|
||||
} // namespace Cheats
|
|
@ -0,0 +1,640 @@
|
|||
// Copyright 2021 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "Core/CheatSearch.h"
|
||||
|
||||
#include <cassert>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
#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<decltype(std::get<static_cast<int>(DataType::U8)>(value.m_value))>>;
|
||||
using should_be_u16 = std::remove_cv_t<
|
||||
std::remove_reference_t<decltype(std::get<static_cast<int>(DataType::U16)>(value.m_value))>>;
|
||||
using should_be_u32 = std::remove_cv_t<
|
||||
std::remove_reference_t<decltype(std::get<static_cast<int>(DataType::U32)>(value.m_value))>>;
|
||||
using should_be_u64 = std::remove_cv_t<
|
||||
std::remove_reference_t<decltype(std::get<static_cast<int>(DataType::U64)>(value.m_value))>>;
|
||||
using should_be_s8 = std::remove_cv_t<
|
||||
std::remove_reference_t<decltype(std::get<static_cast<int>(DataType::S8)>(value.m_value))>>;
|
||||
using should_be_s16 = std::remove_cv_t<
|
||||
std::remove_reference_t<decltype(std::get<static_cast<int>(DataType::S16)>(value.m_value))>>;
|
||||
using should_be_s32 = std::remove_cv_t<
|
||||
std::remove_reference_t<decltype(std::get<static_cast<int>(DataType::S32)>(value.m_value))>>;
|
||||
using should_be_s64 = std::remove_cv_t<
|
||||
std::remove_reference_t<decltype(std::get<static_cast<int>(DataType::S64)>(value.m_value))>>;
|
||||
using should_be_f32 = std::remove_cv_t<
|
||||
std::remove_reference_t<decltype(std::get<static_cast<int>(DataType::F32)>(value.m_value))>>;
|
||||
using should_be_f64 = std::remove_cv_t<
|
||||
std::remove_reference_t<decltype(std::get<static_cast<int>(DataType::F64)>(value.m_value))>>;
|
||||
static_assert(std::is_same_v<u8, should_be_u8>);
|
||||
static_assert(std::is_same_v<u16, should_be_u16>);
|
||||
static_assert(std::is_same_v<u32, should_be_u32>);
|
||||
static_assert(std::is_same_v<u64, should_be_u64>);
|
||||
static_assert(std::is_same_v<s8, should_be_s8>);
|
||||
static_assert(std::is_same_v<s16, should_be_s16>);
|
||||
static_assert(std::is_same_v<s32, should_be_s32>);
|
||||
static_assert(std::is_same_v<s64, should_be_s64>);
|
||||
static_assert(std::is_same_v<float, should_be_f32>);
|
||||
static_assert(std::is_same_v<double, should_be_f64>);
|
||||
|
||||
return static_cast<DataType>(value.m_value.index());
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
static std::vector<u8> ToByteVector(const T& val)
|
||||
{
|
||||
static_assert(std::is_trivially_copyable_v<T>);
|
||||
const auto* const begin = reinterpret_cast<const u8*>(&val);
|
||||
const auto* const end = begin + sizeof(T);
|
||||
return {begin, end};
|
||||
}
|
||||
|
||||
std::vector<u8> Cheats::GetValueAsByteVector(const Cheats::SearchValue& value)
|
||||
{
|
||||
DataType type = GetDataType(value);
|
||||
switch (type)
|
||||
{
|
||||
case Cheats::DataType::U8:
|
||||
return {std::get<u8>(value.m_value)};
|
||||
case Cheats::DataType::U16:
|
||||
return ToByteVector(Common::swap16(std::get<u16>(value.m_value)));
|
||||
case Cheats::DataType::U32:
|
||||
return ToByteVector(Common::swap32(std::get<u32>(value.m_value)));
|
||||
case Cheats::DataType::U64:
|
||||
return ToByteVector(Common::swap64(std::get<u64>(value.m_value)));
|
||||
case Cheats::DataType::S8:
|
||||
return {Common::BitCast<u8>(std::get<s8>(value.m_value))};
|
||||
case Cheats::DataType::S16:
|
||||
return ToByteVector(Common::swap16(Common::BitCast<u16>(std::get<s16>(value.m_value))));
|
||||
case Cheats::DataType::S32:
|
||||
return ToByteVector(Common::swap32(Common::BitCast<u32>(std::get<s32>(value.m_value))));
|
||||
case Cheats::DataType::S64:
|
||||
return ToByteVector(Common::swap64(Common::BitCast<u64>(std::get<s64>(value.m_value))));
|
||||
case Cheats::DataType::F32:
|
||||
return ToByteVector(Common::swap32(Common::BitCast<u32>(std::get<float>(value.m_value))));
|
||||
case Cheats::DataType::F64:
|
||||
return ToByteVector(Common::swap64(Common::BitCast<u64>(std::get<double>(value.m_value))));
|
||||
default:
|
||||
assert(0);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
template <typename T>
|
||||
static PowerPC::TryReadResult<T>
|
||||
TryReadValueFromEmulatedMemory(u32 addr, PowerPC::RequestedAddressSpace space);
|
||||
|
||||
template <>
|
||||
PowerPC::TryReadResult<u8> TryReadValueFromEmulatedMemory(u32 addr,
|
||||
PowerPC::RequestedAddressSpace space)
|
||||
{
|
||||
return PowerPC::HostTryReadU8(addr, space);
|
||||
}
|
||||
|
||||
template <>
|
||||
PowerPC::TryReadResult<u16> TryReadValueFromEmulatedMemory(u32 addr,
|
||||
PowerPC::RequestedAddressSpace space)
|
||||
{
|
||||
return PowerPC::HostTryReadU16(addr, space);
|
||||
}
|
||||
|
||||
template <>
|
||||
PowerPC::TryReadResult<u32> TryReadValueFromEmulatedMemory(u32 addr,
|
||||
PowerPC::RequestedAddressSpace space)
|
||||
{
|
||||
return PowerPC::HostTryReadU32(addr, space);
|
||||
}
|
||||
|
||||
template <>
|
||||
PowerPC::TryReadResult<u64> TryReadValueFromEmulatedMemory(u32 addr,
|
||||
PowerPC::RequestedAddressSpace space)
|
||||
{
|
||||
return PowerPC::HostTryReadU64(addr, space);
|
||||
}
|
||||
|
||||
template <>
|
||||
PowerPC::TryReadResult<s8> TryReadValueFromEmulatedMemory(u32 addr,
|
||||
PowerPC::RequestedAddressSpace space)
|
||||
{
|
||||
auto tmp = PowerPC::HostTryReadU8(addr, space);
|
||||
if (!tmp)
|
||||
return PowerPC::TryReadResult<s8>();
|
||||
return PowerPC::TryReadResult<s8>(tmp.translated, Common::BitCast<s8>(tmp.value));
|
||||
}
|
||||
|
||||
template <>
|
||||
PowerPC::TryReadResult<s16> TryReadValueFromEmulatedMemory(u32 addr,
|
||||
PowerPC::RequestedAddressSpace space)
|
||||
{
|
||||
auto tmp = PowerPC::HostTryReadU16(addr, space);
|
||||
if (!tmp)
|
||||
return PowerPC::TryReadResult<s16>();
|
||||
return PowerPC::TryReadResult<s16>(tmp.translated, Common::BitCast<s16>(tmp.value));
|
||||
}
|
||||
|
||||
template <>
|
||||
PowerPC::TryReadResult<s32> TryReadValueFromEmulatedMemory(u32 addr,
|
||||
PowerPC::RequestedAddressSpace space)
|
||||
{
|
||||
auto tmp = PowerPC::HostTryReadU32(addr, space);
|
||||
if (!tmp)
|
||||
return PowerPC::TryReadResult<s32>();
|
||||
return PowerPC::TryReadResult<s32>(tmp.translated, Common::BitCast<s32>(tmp.value));
|
||||
}
|
||||
|
||||
template <>
|
||||
PowerPC::TryReadResult<s64> TryReadValueFromEmulatedMemory(u32 addr,
|
||||
PowerPC::RequestedAddressSpace space)
|
||||
{
|
||||
auto tmp = PowerPC::HostTryReadU64(addr, space);
|
||||
if (!tmp)
|
||||
return PowerPC::TryReadResult<s64>();
|
||||
return PowerPC::TryReadResult<s64>(tmp.translated, Common::BitCast<s64>(tmp.value));
|
||||
}
|
||||
|
||||
template <>
|
||||
PowerPC::TryReadResult<float> TryReadValueFromEmulatedMemory(u32 addr,
|
||||
PowerPC::RequestedAddressSpace space)
|
||||
{
|
||||
return PowerPC::HostTryReadF32(addr, space);
|
||||
}
|
||||
|
||||
template <>
|
||||
PowerPC::TryReadResult<double> TryReadValueFromEmulatedMemory(u32 addr,
|
||||
PowerPC::RequestedAddressSpace space)
|
||||
{
|
||||
return PowerPC::HostTryReadF64(addr, space);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
template <typename T>
|
||||
Common::Result<Cheats::SearchErrorCode, std::vector<Cheats::SearchResult<T>>>
|
||||
Cheats::NewSearch(const std::vector<Cheats::MemoryRange>& memory_ranges,
|
||||
PowerPC::RequestedAddressSpace address_space, bool aligned,
|
||||
const std::function<bool(const T& value)>& validator)
|
||||
{
|
||||
const u32 data_size = sizeof(T);
|
||||
std::vector<Cheats::SearchResult<T>> 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<T>(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 <typename T>
|
||||
Common::Result<Cheats::SearchErrorCode, std::vector<Cheats::SearchResult<T>>>
|
||||
Cheats::NextSearch(const std::vector<Cheats::SearchResult<T>>& previous_results,
|
||||
PowerPC::RequestedAddressSpace address_space,
|
||||
const std::function<bool(const T& new_value, const T& old_value)>& validator)
|
||||
{
|
||||
std::vector<Cheats::SearchResult<T>> 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<T>(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 <typename T>
|
||||
Cheats::CheatSearchSession<T>::CheatSearchSession(std::vector<MemoryRange> memory_ranges,
|
||||
PowerPC::RequestedAddressSpace address_space,
|
||||
bool aligned)
|
||||
: m_memory_ranges(std::move(memory_ranges)), m_address_space(address_space), m_aligned(aligned)
|
||||
{
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
Cheats::CheatSearchSession<T>::CheatSearchSession(const CheatSearchSession& session) = default;
|
||||
|
||||
template <typename T>
|
||||
Cheats::CheatSearchSession<T>::CheatSearchSession(CheatSearchSession&& session) = default;
|
||||
|
||||
template <typename T>
|
||||
Cheats::CheatSearchSession<T>&
|
||||
Cheats::CheatSearchSession<T>::operator=(const CheatSearchSession& session) = default;
|
||||
|
||||
template <typename T>
|
||||
Cheats::CheatSearchSession<T>&
|
||||
Cheats::CheatSearchSession<T>::operator=(CheatSearchSession&& session) = default;
|
||||
|
||||
template <typename T>
|
||||
Cheats::CheatSearchSession<T>::~CheatSearchSession() = default;
|
||||
|
||||
template <typename T>
|
||||
void Cheats::CheatSearchSession<T>::SetCompareType(CompareType compare_type)
|
||||
{
|
||||
m_compare_type = compare_type;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void Cheats::CheatSearchSession<T>::SetFilterType(FilterType filter_type)
|
||||
{
|
||||
m_filter_type = filter_type;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
static std::optional<T> ParseValue(const std::string& str)
|
||||
{
|
||||
if (str.empty())
|
||||
return std::nullopt;
|
||||
|
||||
T tmp;
|
||||
if (TryParse(str, &tmp))
|
||||
return tmp;
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
bool Cheats::CheatSearchSession<T>::SetValueFromString(const std::string& value_as_string)
|
||||
{
|
||||
m_value = ParseValue<T>(value_as_string);
|
||||
return m_value.has_value();
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void Cheats::CheatSearchSession<T>::ResetResults()
|
||||
{
|
||||
m_first_search_done = false;
|
||||
m_search_results.clear();
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
static std::function<bool(const T& new_value)>
|
||||
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 <typename T>
|
||||
static std::function<bool(const T& old_value, const T& new_value)>
|
||||
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 <typename T>
|
||||
Cheats::SearchErrorCode Cheats::CheatSearchSession<T>::RunSearch()
|
||||
{
|
||||
Common::Result<SearchErrorCode, std::vector<SearchResult<T>>> result =
|
||||
Cheats::SearchErrorCode::InvalidParameters;
|
||||
if (m_filter_type == FilterType::CompareAgainstSpecificValue)
|
||||
{
|
||||
if (!m_value)
|
||||
return Cheats::SearchErrorCode::InvalidParameters;
|
||||
|
||||
auto func = MakeCompareFunctionForSpecificValue<T>(m_compare_type, *m_value);
|
||||
if (m_first_search_done)
|
||||
{
|
||||
result = Cheats::NextSearch<T>(
|
||||
m_search_results, m_address_space,
|
||||
[&func](const T& new_value, const T& old_value) { return func(new_value); });
|
||||
}
|
||||
else
|
||||
{
|
||||
result = Cheats::NewSearch<T>(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<T>(m_search_results, m_address_space,
|
||||
MakeCompareFunctionForLastValue<T>(m_compare_type));
|
||||
}
|
||||
else if (m_filter_type == FilterType::DoNotFilter)
|
||||
{
|
||||
if (m_first_search_done)
|
||||
{
|
||||
result = Cheats::NextSearch<T>(m_search_results, m_address_space,
|
||||
[](const T& v1, const T& v2) { return true; });
|
||||
}
|
||||
else
|
||||
{
|
||||
result = Cheats::NewSearch<T>(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 <typename T>
|
||||
size_t Cheats::CheatSearchSession<T>::GetMemoryRangeCount()
|
||||
{
|
||||
return m_memory_ranges.size();
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
Cheats::MemoryRange Cheats::CheatSearchSession<T>::GetMemoryRange(size_t index)
|
||||
{
|
||||
return m_memory_ranges[index];
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
PowerPC::RequestedAddressSpace Cheats::CheatSearchSession<T>::GetAddressSpace()
|
||||
{
|
||||
return m_address_space;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
Cheats::DataType Cheats::CheatSearchSession<T>::GetDataType()
|
||||
{
|
||||
return Cheats::GetDataType(Cheats::SearchValue{T(0)});
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
bool Cheats::CheatSearchSession<T>::GetAligned()
|
||||
{
|
||||
return m_aligned;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
size_t Cheats::CheatSearchSession<T>::GetResultCount() const
|
||||
{
|
||||
return m_search_results.size();
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
size_t Cheats::CheatSearchSession<T>::GetValidValueCount() const
|
||||
{
|
||||
const auto& results = m_search_results;
|
||||
size_t count = 0;
|
||||
for (const auto& r : results)
|
||||
{
|
||||
if (r.IsValueValid())
|
||||
++count;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
u32 Cheats::CheatSearchSession<T>::GetResultAddress(size_t index) const
|
||||
{
|
||||
return m_search_results[index].m_address;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
T Cheats::CheatSearchSession<T>::GetResultValue(size_t index) const
|
||||
{
|
||||
return m_search_results[index].m_value;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
Cheats::SearchValue Cheats::CheatSearchSession<T>::GetResultValueAsSearchValue(size_t index) const
|
||||
{
|
||||
return Cheats::SearchValue{m_search_results[index].m_value};
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
std::string Cheats::CheatSearchSession<T>::GetResultValueAsString(size_t index, bool hex) const
|
||||
{
|
||||
if (GetResultValueState(index) == Cheats::SearchResultValueState::AddressNotAccessible)
|
||||
return "(inaccessible)";
|
||||
|
||||
if (hex)
|
||||
{
|
||||
if constexpr (std::is_same_v<T, float>)
|
||||
return fmt::format("0x{0:08x}", Common::BitCast<u32>(m_search_results[index].m_value));
|
||||
else if constexpr (std::is_same_v<T, double>)
|
||||
return fmt::format("0x{0:016x}", Common::BitCast<u64>(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 <typename T>
|
||||
Cheats::SearchResultValueState
|
||||
Cheats::CheatSearchSession<T>::GetResultValueState(size_t index) const
|
||||
{
|
||||
return m_search_results[index].m_value_state;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
bool Cheats::CheatSearchSession<T>::WasFirstSearchDone() const
|
||||
{
|
||||
return m_first_search_done;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
std::unique_ptr<Cheats::CheatSearchSessionBase> Cheats::CheatSearchSession<T>::Clone() const
|
||||
{
|
||||
return std::make_unique<Cheats::CheatSearchSession<T>>(*this);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
std::unique_ptr<Cheats::CheatSearchSessionBase>
|
||||
Cheats::CheatSearchSession<T>::ClonePartial(const std::vector<size_t>& result_indices) const
|
||||
{
|
||||
const auto& results = m_search_results;
|
||||
std::vector<SearchResult<T>> 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<Cheats::CheatSearchSession<T>>(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<u8>;
|
||||
template class Cheats::CheatSearchSession<u16>;
|
||||
template class Cheats::CheatSearchSession<u32>;
|
||||
template class Cheats::CheatSearchSession<u64>;
|
||||
template class Cheats::CheatSearchSession<s8>;
|
||||
template class Cheats::CheatSearchSession<s16>;
|
||||
template class Cheats::CheatSearchSession<s32>;
|
||||
template class Cheats::CheatSearchSession<s64>;
|
||||
template class Cheats::CheatSearchSession<float>;
|
||||
template class Cheats::CheatSearchSession<double>;
|
||||
|
||||
std::unique_ptr<Cheats::CheatSearchSessionBase>
|
||||
Cheats::MakeSession(std::vector<MemoryRange> memory_ranges,
|
||||
PowerPC::RequestedAddressSpace address_space, bool aligned, DataType data_type)
|
||||
{
|
||||
switch (data_type)
|
||||
{
|
||||
case Cheats::DataType::U8:
|
||||
return std::make_unique<CheatSearchSession<u8>>(std::move(memory_ranges), address_space,
|
||||
aligned);
|
||||
case Cheats::DataType::U16:
|
||||
return std::make_unique<CheatSearchSession<u16>>(std::move(memory_ranges), address_space,
|
||||
aligned);
|
||||
case Cheats::DataType::U32:
|
||||
return std::make_unique<CheatSearchSession<u32>>(std::move(memory_ranges), address_space,
|
||||
aligned);
|
||||
case Cheats::DataType::U64:
|
||||
return std::make_unique<CheatSearchSession<u64>>(std::move(memory_ranges), address_space,
|
||||
aligned);
|
||||
case Cheats::DataType::S8:
|
||||
return std::make_unique<CheatSearchSession<s8>>(std::move(memory_ranges), address_space,
|
||||
aligned);
|
||||
case Cheats::DataType::S16:
|
||||
return std::make_unique<CheatSearchSession<s16>>(std::move(memory_ranges), address_space,
|
||||
aligned);
|
||||
case Cheats::DataType::S32:
|
||||
return std::make_unique<CheatSearchSession<s32>>(std::move(memory_ranges), address_space,
|
||||
aligned);
|
||||
case Cheats::DataType::S64:
|
||||
return std::make_unique<CheatSearchSession<s64>>(std::move(memory_ranges), address_space,
|
||||
aligned);
|
||||
case Cheats::DataType::F32:
|
||||
return std::make_unique<CheatSearchSession<float>>(std::move(memory_ranges), address_space,
|
||||
aligned);
|
||||
case Cheats::DataType::F64:
|
||||
return std::make_unique<CheatSearchSession<double>>(std::move(memory_ranges), address_space,
|
||||
aligned);
|
||||
default:
|
||||
assert(0);
|
||||
return nullptr;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,219 @@
|
|||
// Copyright 2021 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
#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<u8, u16, u32, u64, s8, s16, s32, s64, float, double> m_value;
|
||||
};
|
||||
|
||||
enum class SearchResultValueState : u8
|
||||
{
|
||||
ValueFromPhysicalMemory,
|
||||
ValueFromVirtualMemory,
|
||||
AddressNotAccessible,
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
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<u8>, 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<u8> 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 <typename T>
|
||||
Common::Result<SearchErrorCode, std::vector<SearchResult<T>>>
|
||||
NewSearch(const std::vector<MemoryRange>& memory_ranges,
|
||||
PowerPC::RequestedAddressSpace address_space, bool aligned,
|
||||
const std::function<bool(const T& value)>& validator);
|
||||
|
||||
// Refresh the values for the given results in the given address space, only keeping values for
|
||||
// which the given validator returns true.
|
||||
template <typename T>
|
||||
Common::Result<SearchErrorCode, std::vector<SearchResult<T>>>
|
||||
NextSearch(const std::vector<SearchResult<T>>& previous_results,
|
||||
PowerPC::RequestedAddressSpace address_space,
|
||||
const std::function<bool(const T& new_value, const T& old_value)>& 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<CheatSearchSessionBase> 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<CheatSearchSessionBase>
|
||||
ClonePartial(const std::vector<size_t>& result_indices) const = 0;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
class CheatSearchSession : public CheatSearchSessionBase
|
||||
{
|
||||
public:
|
||||
CheatSearchSession(std::vector<MemoryRange> 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<CheatSearchSessionBase> Clone() const override;
|
||||
std::unique_ptr<CheatSearchSessionBase>
|
||||
ClonePartial(const std::vector<size_t>& result_indices) const override;
|
||||
|
||||
private:
|
||||
std::vector<SearchResult<T>> m_search_results;
|
||||
std::vector<MemoryRange> m_memory_ranges;
|
||||
PowerPC::RequestedAddressSpace m_address_space;
|
||||
CompareType m_compare_type = CompareType::Equal;
|
||||
FilterType m_filter_type = FilterType::DoNotFilter;
|
||||
std::optional<T> m_value = std::nullopt;
|
||||
bool m_aligned;
|
||||
bool m_first_search_done = false;
|
||||
};
|
||||
|
||||
std::unique_ptr<CheatSearchSessionBase> MakeSession(std::vector<MemoryRange> memory_ranges,
|
||||
PowerPC::RequestedAddressSpace address_space,
|
||||
bool aligned, DataType data_type);
|
||||
} // namespace Cheats
|
|
@ -163,6 +163,8 @@
|
|||
<ClInclude Include="Core\Boot\ElfTypes.h" />
|
||||
<ClInclude Include="Core\BootManager.h" />
|
||||
<ClInclude Include="Core\CheatCodes.h" />
|
||||
<ClInclude Include="Core\CheatGeneration.h" />
|
||||
<ClInclude Include="Core\CheatSearch.h" />
|
||||
<ClInclude Include="Core\CommonTitles.h" />
|
||||
<ClInclude Include="Core\Config\DefaultLocale.h" />
|
||||
<ClInclude Include="Core\Config\FreeLookSettings.h" />
|
||||
|
@ -743,6 +745,8 @@
|
|||
<ClCompile Include="Core\Boot\DolReader.cpp" />
|
||||
<ClCompile Include="Core\Boot\ElfReader.cpp" />
|
||||
<ClCompile Include="Core\BootManager.cpp" />
|
||||
<ClCompile Include="Core\CheatGeneration.cpp" />
|
||||
<ClCompile Include="Core\CheatSearch.cpp" />
|
||||
<ClCompile Include="Core\Config\DefaultLocale.cpp" />
|
||||
<ClCompile Include="Core\Config\FreeLookSettings.cpp" />
|
||||
<ClCompile Include="Core\Config\GraphicsSettings.cpp" />
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,187 @@
|
|||
// Copyright 2021 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "DolphinQt/CheatSearchFactoryWidget.h"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <QButtonGroup>
|
||||
#include <QCheckBox>
|
||||
#include <QComboBox>
|
||||
#include <QGroupBox>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QPushButton>
|
||||
#include <QRadioButton>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#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<Cheats::MemoryRange> 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<u32>(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<Cheats::DataType>();
|
||||
auto session = Cheats::MakeSession(std::move(memory_ranges), address_space, aligned, data_type);
|
||||
if (session)
|
||||
emit NewSessionCreated(*session);
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
// Copyright 2021 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
#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;
|
||||
};
|
|
@ -0,0 +1,529 @@
|
|||
// Copyright 2021 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "DolphinQt/CheatSearchWidget.h"
|
||||
|
||||
#include <cassert>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QComboBox>
|
||||
#include <QCursor>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QMenu>
|
||||
#include <QPushButton>
|
||||
#include <QSignalBlocker>
|
||||
#include <QString>
|
||||
#include <QTableWidget>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include <fmt/format.h>
|
||||
|
||||
#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<Cheats::CheatSearchSessionBase> 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<Cheats::FilterType>();
|
||||
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<Cheats::CompareType>());
|
||||
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<int>(old_count - new_count));
|
||||
const QString remain_str = tr("%n address(es) remain.", "", static_cast<int>(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<int>(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<int>(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<int>(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<Cheats::CheatSearchSessionBase> tmp;
|
||||
if (too_many_results)
|
||||
{
|
||||
std::vector<size_t> 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<Cheats::FilterType>();
|
||||
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<u32>(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<u32>(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<u32>(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<u32>(i));
|
||||
m_address_table->setItem(i, ADDRESS_TABLE_COLUMN_INDEX_CURRENT_VALUE, current_value_item);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
// Copyright 2021 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#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<Cheats::CheatSearchSessionBase> 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<Cheats::CheatSearchSessionBase> m_session;
|
||||
|
||||
// storage for the 'Current Value' column's data
|
||||
std::unordered_map<u32, std::string> 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<u32, CheatSearchTableUserData> 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;
|
||||
};
|
|
@ -3,152 +3,24 @@
|
|||
|
||||
#include "DolphinQt/CheatsManager.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <functional>
|
||||
|
||||
#include <QComboBox>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QGroupBox>
|
||||
#include <QHeaderView>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QMenu>
|
||||
#include <QPushButton>
|
||||
#include <QRadioButton>
|
||||
#include <QSplitter>
|
||||
#include <QTabWidget>
|
||||
#include <QTableWidget>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#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<u8>{static_cast<u8>(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 <typename T>
|
||||
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<DataType>(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<bool(u32)> 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<CompareType>(m_match_operation->currentIndex());
|
||||
|
||||
const int base =
|
||||
(m_match_decimal->isChecked() ? 10 : (m_match_hexadecimal->isChecked() ? 16 : 8));
|
||||
|
||||
bool conversion_succeeded = false;
|
||||
std::function<bool(u32)> matches_func;
|
||||
switch (static_cast<DataType>(m_match_length->currentIndex()))
|
||||
{
|
||||
case DataType::Byte:
|
||||
{
|
||||
u8 comparison_value = text.toUShort(&conversion_succeeded, base) & 0xFF;
|
||||
matches_func = [=](u32 addr) {
|
||||
return Compare<u8>(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<char*>(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<bool(u32)> 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<DataType>(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<bool(u32)> 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<int>(m_results.size())));
|
||||
m_match_table->setRowCount(static_cast<int>(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<int>(i));
|
||||
value_item->setData(INDEX_ROLE, static_cast<int>(i));
|
||||
|
||||
m_match_table->setItem(static_cast<int>(i), 0, address_item);
|
||||
m_match_table->setItem(static_cast<int>(i), 1, value_item);
|
||||
}
|
||||
|
||||
m_watch_table->setRowCount(static_cast<int>(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<int>(i));
|
||||
name_item->setData(COLUMN_ROLE, 0);
|
||||
|
||||
address_item->setData(INDEX_ROLE, static_cast<int>(i));
|
||||
address_item->setData(COLUMN_ROLE, 1);
|
||||
|
||||
lock_item->setData(INDEX_ROLE, static_cast<int>(i));
|
||||
lock_item->setData(COLUMN_ROLE, 2);
|
||||
|
||||
value_item->setData(INDEX_ROLE, static_cast<int>(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<int>(i), 0, name_item);
|
||||
m_watch_table->setItem(static_cast<int>(i), 1, address_item);
|
||||
m_watch_table->setItem(static_cast<int>(i), 2, lock_item);
|
||||
m_watch_table->setItem(static_cast<int>(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);
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
#include <QDialog>
|
||||
|
@ -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<bool(u32)> 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<Result> m_results;
|
||||
std::vector<Result> 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;
|
||||
};
|
||||
|
|
|
@ -45,6 +45,8 @@
|
|||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="AboutDialog.cpp" />
|
||||
<ClCompile Include="CheatSearchFactoryWidget.cpp" />
|
||||
<ClCompile Include="CheatSearchWidget.cpp" />
|
||||
<ClCompile Include="CheatsManager.cpp" />
|
||||
<ClCompile Include="Config\ARCodeWidget.cpp" />
|
||||
<ClCompile Include="Config\CheatCodeEditor.cpp" />
|
||||
|
@ -168,6 +170,7 @@
|
|||
<ClCompile Include="QtUtils\FlowLayout.cpp" />
|
||||
<ClCompile Include="QtUtils\ImageConverter.cpp" />
|
||||
<ClCompile Include="QtUtils\ModalMessageBox.cpp" />
|
||||
<ClCompile Include="QtUtils\PartiallyClosableTabWidget.cpp" />
|
||||
<ClCompile Include="QtUtils\UTF8CodePointCountValidator.cpp" />
|
||||
<ClCompile Include="QtUtils\WindowActivationEventFilter.cpp" />
|
||||
<ClCompile Include="QtUtils\WinIconHelper.cpp" />
|
||||
|
@ -227,6 +230,8 @@
|
|||
<ClInclude Include="Translation.h" />
|
||||
<ClInclude Include="WiiUpdate.h" />
|
||||
<QtMoc Include="AboutDialog.h" />
|
||||
<QtMoc Include="CheatSearchFactoryWidget.h" />
|
||||
<QtMoc Include="CheatSearchWidget.h" />
|
||||
<QtMoc Include="CheatsManager.h" />
|
||||
<QtMoc Include="Config\ARCodeWidget.h" />
|
||||
<QtMoc Include="Config\CheatWarningWidget.h" />
|
||||
|
@ -340,6 +345,7 @@
|
|||
<QtMoc Include="QtUtils\ElidedButton.h" />
|
||||
<QtMoc Include="QtUtils\FileOpenEventFilter.h" />
|
||||
<QtMoc Include="QtUtils\ParallelProgressDialog.h" />
|
||||
<QtMoc Include="QtUtils\PartiallyClosableTabWidget.h" />
|
||||
<QtMoc Include="QtUtils\UTF8CodePointCountValidator.h" />
|
||||
<QtMoc Include="QtUtils\WindowActivationEventFilter.h" />
|
||||
<QtMoc Include="RenderWidget.h" />
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
// Copyright 2021 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "DolphinQt/QtUtils/PartiallyClosableTabWidget.h"
|
||||
|
||||
#include <QStyle>
|
||||
#include <QTabBar>
|
||||
|
||||
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);
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
// Copyright 2021 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QTabWidget>
|
||||
|
||||
class QEvent;
|
||||
|
||||
class PartiallyClosableTabWidget : public QTabWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
PartiallyClosableTabWidget(QWidget* parent = nullptr);
|
||||
|
||||
void setTabUnclosable(int index);
|
||||
};
|
Loading…
Reference in New Issue