mirror of
https://git.eden-emu.dev/eden-emu/eden.git
synced 2026-11-02 12:07:02 +00:00
[cheats] add runtime cheats toggles with master code support (#4334)
add per-cheat runtime toggles (thread-safe). add master code handling (master code cannot be disabled, as noted in Atmosphere's implementation). add configurable global / per-game initial states for cheat entries. remove fixed opcode limits and do some fixes to text parsing / file loading. do not run debug log while cheat logging is not enabled, to reduce performance impacts. update the `Using Cheats` document to reflect the changes. - [x] I have read and followed the [Contribution Guidelines](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/CONTRIBUTING.md#code-contributions). - [x] I have read and followed the [AI Policy](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/docs/policies/AI.md) - [x] I have read and followed the [Coding Guidelines](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/docs/policies/Coding.md) to the best of my ability. ------------------- Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/4334 Reviewed-by: lizzie <lizzie@eden-emu.dev> Reviewed-by: CamilleLaVey <camillelavey99@gmail.com> Reviewed-by: John <john@eden-emu.dev>
This commit is contained in:
@@ -34,6 +34,13 @@ import org.yuzu.yuzu_emu.applets.web.WebBrowser
|
||||
* with the native side of the Yuzu code.
|
||||
*/
|
||||
object NativeLibrary {
|
||||
@Keep
|
||||
class RuntimeCheat(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
val enabled: Boolean,
|
||||
val isMaster: Boolean
|
||||
)
|
||||
@Keep
|
||||
data class UpdateResult(
|
||||
var tag: String = "",
|
||||
@@ -197,6 +204,9 @@ object NativeLibrary {
|
||||
*/
|
||||
external fun isPaused(): Boolean
|
||||
|
||||
external fun getRuntimeCheats(): Array<RuntimeCheat>
|
||||
external fun setRuntimeCheatEnabled(id: Int, enabled: Boolean): Boolean
|
||||
|
||||
/**
|
||||
* Returns the performance stats for the current game
|
||||
*/
|
||||
|
||||
+1
@@ -13,6 +13,7 @@ enum class BooleanSetting(override val key: String) : AbstractBooleanSetting {
|
||||
FASTMEM("cpuopt_fastmem"),
|
||||
FASTMEM_EXCLUSIVES("cpuopt_fastmem_exclusives"),
|
||||
CORE_SYNC_CORE_SPEED("sync_core_speed"),
|
||||
ENABLE_CHEATS_BY_DEFAULT("enable_cheats_by_default"),
|
||||
RENDERER_USE_SPEED_LIMIT("use_speed_limit"),
|
||||
USE_CUSTOM_CPU_TICKS("use_custom_cpu_ticks"),
|
||||
SKIP_CPU_INNER_INVALIDATION("skip_cpu_inner_invalidation"),
|
||||
|
||||
+7
@@ -316,6 +316,13 @@ abstract class SettingsItem(
|
||||
descriptionId = R.string.use_sync_core_description
|
||||
)
|
||||
)
|
||||
put(
|
||||
SwitchSetting(
|
||||
BooleanSetting.ENABLE_CHEATS_BY_DEFAULT,
|
||||
titleId = R.string.enable_cheats_by_default,
|
||||
descriptionId = R.string.enable_cheats_by_default_description
|
||||
)
|
||||
)
|
||||
put(
|
||||
SingleChoiceSetting(
|
||||
IntSetting.REGION_INDEX,
|
||||
|
||||
+1
@@ -510,6 +510,7 @@ class SettingsFragmentPresenter(
|
||||
add(IntSetting.FAST_CPU_TIME.key)
|
||||
add(IntSetting.FAST_GPU_TIME.key)
|
||||
add(BooleanSetting.CORE_SYNC_CORE_SPEED.key)
|
||||
add(BooleanSetting.ENABLE_CHEATS_BY_DEFAULT.key)
|
||||
|
||||
add(IntSetting.MEMORY_LAYOUT.key)
|
||||
add(BooleanSetting.USE_CUSTOM_CPU_TICKS.key)
|
||||
|
||||
@@ -734,6 +734,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
||||
|
||||
binding.inGameMenu.menu.findItem(R.id.menu_quick_settings)?.isVisible =
|
||||
BooleanSetting.ENABLE_QUICK_SETTINGS.getBoolean()
|
||||
binding.inGameMenu.menu.findItem(R.id.menu_cheats)?.isVisible = false
|
||||
|
||||
binding.pausedIcon.setOnClickListener {
|
||||
if (this::emulationState.isInitialized && emulationState.isPaused) {
|
||||
@@ -817,6 +818,12 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
||||
|
||||
R.id.menu_load_amiibo -> handleLoadAmiiboSelection()
|
||||
|
||||
R.id.menu_cheats -> {
|
||||
binding.drawerLayout.closeDrawer(binding.inGameMenu)
|
||||
showRuntimeCheatDialog()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.menu_controls -> {
|
||||
val action = HomeNavigationDirections.actionGlobalSettingsActivity(
|
||||
null,
|
||||
@@ -962,6 +969,8 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
||||
|
||||
emulationState.updateSurface()
|
||||
|
||||
binding.inGameMenu.menu.findItem(R.id.menu_cheats)?.isVisible =
|
||||
NativeLibrary.getRuntimeCheats().isNotEmpty()
|
||||
updateShowStatsOverlay()
|
||||
updateSocOverlay()
|
||||
|
||||
@@ -987,6 +996,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
||||
}
|
||||
emulationViewModel.isEmulationStopping.collect(viewLifecycleOwner) {
|
||||
if (it) {
|
||||
binding.inGameMenu.menu.findItem(R.id.menu_cheats)?.isVisible = false
|
||||
binding.loadingText.setText(R.string.shutting_down)
|
||||
ViewUtils.showView(binding.loadingIndicator)
|
||||
ViewUtils.hideView(binding.inputContainer)
|
||||
@@ -1119,6 +1129,45 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
||||
NativePostProcessing.persist()
|
||||
}
|
||||
|
||||
private fun showRuntimeCheatDialog() {
|
||||
val cheats = NativeLibrary.getRuntimeCheats()
|
||||
if (cheats.isEmpty()) return
|
||||
|
||||
val names = cheats.map { it.name.ifEmpty { getString(R.string.unnamed_cheat) } }.toTypedArray()
|
||||
lateinit var dialog: androidx.appcompat.app.AlertDialog
|
||||
dialog = MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.cheats)
|
||||
.setMultiChoiceItems(names, cheats.map { it.enabled }.toBooleanArray()) { _, which, enabled ->
|
||||
if (cheats[which].isMaster) {
|
||||
dialog.listView.setItemChecked(which, true)
|
||||
} else if (!NativeLibrary.setRuntimeCheatEnabled(cheats[which].id, enabled)) {
|
||||
dialog.listView.setItemChecked(which, !enabled)
|
||||
}
|
||||
}
|
||||
.setNegativeButton(R.string.disable_all_cheats) { _, _ ->
|
||||
cheats.forEach {
|
||||
if (!it.isMaster) NativeLibrary.setRuntimeCheatEnabled(it.id, false)
|
||||
}
|
||||
}
|
||||
.setNeutralButton(R.string.enable_all_cheats) { _, _ ->
|
||||
cheats.forEach {
|
||||
if (!it.isMaster) NativeLibrary.setRuntimeCheatEnabled(it.id, true)
|
||||
}
|
||||
}
|
||||
.setPositiveButton(R.string.done, null)
|
||||
.create()
|
||||
dialog.setOnShowListener {
|
||||
val listView = dialog.listView
|
||||
fun updateMasterVisuals() = repeat(listView.childCount) { index ->
|
||||
listView.getChildAt(index).alpha =
|
||||
if (cheats[listView.firstVisiblePosition + index].isMaster) 0.38f else 1f
|
||||
}
|
||||
listView.setOnScrollChangeListener { _, _, _, _, _ -> updateMasterVisuals() }
|
||||
updateMasterVisuals()
|
||||
}
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
private fun addQuickSettings() {
|
||||
binding.quickSettingsSheet.apply {
|
||||
val container = binding.quickSettingsSheet.findViewById<ViewGroup>(R.id.quick_settings_container)
|
||||
|
||||
@@ -67,6 +67,7 @@ extern "C" {
|
||||
#include "core/file_sys/submission_package.h"
|
||||
#include "core/file_sys/vfs/vfs.h"
|
||||
#include "core/file_sys/vfs/vfs_real.h"
|
||||
#include "core/memory/dmnt_cheat_types.h"
|
||||
#include "core/frontend/applets/cabinet.h"
|
||||
#include "core/frontend/applets/controller.h"
|
||||
#include "core/frontend/applets/error.h"
|
||||
@@ -946,6 +947,31 @@ jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isPaused(JNIEnv* env, jclass claz
|
||||
return static_cast<jboolean>(EmulationSession::GetInstance().IsPaused());
|
||||
}
|
||||
|
||||
jobjectArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getRuntimeCheats(JNIEnv* env, jclass clazz) {
|
||||
const auto cheats = EmulationSession::GetInstance().System().GetRuntimeCheats();
|
||||
const auto runtime_cheat_class = env->FindClass("org/yuzu/yuzu_emu/NativeLibrary$RuntimeCheat");
|
||||
const auto constructor = env->GetMethodID(runtime_cheat_class, "<init>", "(ILjava/lang/String;ZZ)V");
|
||||
auto result = env->NewObjectArray(static_cast<jsize>(cheats.size()), runtime_cheat_class, nullptr);
|
||||
for (jsize i = 0; i < static_cast<jsize>(cheats.size()); ++i) {
|
||||
const auto& cheat = cheats[i];
|
||||
const auto name = Common::Android::ToJString(env, cheat.name);
|
||||
const auto item = env->NewObject(runtime_cheat_class, constructor, static_cast<jint>(cheat.id), name,
|
||||
static_cast<jboolean>(cheat.enabled),
|
||||
static_cast<jboolean>(cheat.is_master));
|
||||
env->SetObjectArrayElement(result, i, item);
|
||||
env->DeleteLocalRef(name);
|
||||
env->DeleteLocalRef(item);
|
||||
}
|
||||
env->DeleteLocalRef(runtime_cheat_class);
|
||||
return result;
|
||||
}
|
||||
|
||||
jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_setRuntimeCheatEnabled(JNIEnv* env, jclass clazz,
|
||||
jint id, jboolean enabled) {
|
||||
return static_cast<jboolean>(EmulationSession::GetInstance().System().SetCheatEnabled(
|
||||
static_cast<u32>(id), static_cast<bool>(enabled)));
|
||||
}
|
||||
|
||||
jbyteArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getAppletCaptureBuffer(JNIEnv* env, jclass clazz) {
|
||||
using namespace VideoCore::Capture;
|
||||
|
||||
|
||||
@@ -43,6 +43,10 @@
|
||||
android:icon="@drawable/ic_nfc"
|
||||
android:title="@string/load_amiibo" />
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_cheats"
|
||||
android:icon="@drawable/ic_code"
|
||||
android:title="@string/cheats" />
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_overlay_controls"
|
||||
|
||||
@@ -840,6 +840,12 @@
|
||||
<string name="preferences_graphics">Graphics</string>
|
||||
<string name="preferences_graphics_description">Accuracy level, resolution, shader cache</string>
|
||||
<string name="quick_settings">Quick Settings</string>
|
||||
<string name="cheats">Cheats</string>
|
||||
<string name="enable_all_cheats">Enable All Cheats</string>
|
||||
<string name="disable_all_cheats">Disable All Cheats</string>
|
||||
<string name="unnamed_cheat">Unnamed Cheat</string>
|
||||
<string name="enable_cheats_by_default">Enable cheats by default</string>
|
||||
<string name="enable_cheats_by_default_description">Enable regular cheats when a game starts</string>
|
||||
<string name="enable_quick_settings">Enable Quick Settings</string>
|
||||
<string name="enable_quick_settings_description">Allow access to quick settings menu via swipe and menu button</string>
|
||||
<string name="preferences_audio">Audio</string>
|
||||
|
||||
@@ -418,6 +418,10 @@ void SetGlobalFilter(const Filter& filter) {
|
||||
logging_instance->filter = filter;
|
||||
}
|
||||
|
||||
bool IsMessageEnabled(Class log_class, Level log_level) {
|
||||
return logging_instance && logging_instance->filter.CheckMessage(log_class, log_level);
|
||||
}
|
||||
|
||||
void SetColorConsoleBackendEnabled(bool enabled) {
|
||||
if (logging_instance)
|
||||
logging_instance->color_console_backend.enabled = enabled;
|
||||
|
||||
@@ -138,6 +138,7 @@ void Stop();
|
||||
|
||||
/// The global filter will prevent any messages from even being processed if they are filtered.
|
||||
void SetGlobalFilter(const Filter& filter);
|
||||
[[nodiscard]] bool IsMessageEnabled(Class log_class, Level log_level);
|
||||
void SetColorConsoleBackendEnabled(bool enabled);
|
||||
|
||||
} // namespace Common::Log
|
||||
|
||||
@@ -247,6 +247,7 @@ struct Values {
|
||||
|
||||
SwitchableSetting<bool> sync_core_speed{linkage, false, "sync_core_speed", Category::Core,
|
||||
Specialization::Default};
|
||||
SwitchableSetting<bool> enable_cheats_by_default{linkage, true, "enable_cheats_by_default", Category::Core};
|
||||
|
||||
// Cpu
|
||||
SwitchableSetting<CpuBackend, true> cpu_backend{linkage,
|
||||
|
||||
@@ -786,6 +786,17 @@ void System::RegisterCheatList(const std::vector<Memory::CheatEntry>& list,
|
||||
impl->cheat_engine->SetMainMemoryParameters(main_region_begin, main_region_size);
|
||||
}
|
||||
|
||||
std::vector<Memory::RuntimeCheatInfo> System::GetRuntimeCheats() const {
|
||||
if (!impl->cheat_engine) {
|
||||
return {};
|
||||
}
|
||||
return impl->cheat_engine->GetCheats();
|
||||
}
|
||||
|
||||
bool System::SetCheatEnabled(u32 cheat_id, bool enabled) {
|
||||
return impl->cheat_engine && impl->cheat_engine->SetCheatEnabled(cheat_id, enabled);
|
||||
}
|
||||
|
||||
void System::SetFrontendAppletSet(Service::AM::Frontend::FrontendAppletSet&& set) {
|
||||
impl->frontend_applets.SetFrontendAppletSet(std::move(set));
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ enum class ResultStatus : u16;
|
||||
|
||||
namespace Core::Memory {
|
||||
struct CheatEntry;
|
||||
struct RuntimeCheatInfo;
|
||||
class Memory;
|
||||
} // namespace Core::Memory
|
||||
|
||||
@@ -342,6 +343,8 @@ public:
|
||||
void RegisterCheatList(const std::vector<Memory::CheatEntry>& list,
|
||||
const std::array<u8, 0x20>& build_id, u64 main_region_begin,
|
||||
u64 main_region_size);
|
||||
[[nodiscard]] std::vector<Memory::RuntimeCheatInfo> GetRuntimeCheats() const;
|
||||
bool SetCheatEnabled(u32 cheat_id, bool enabled);
|
||||
|
||||
void SetFrontendAppletSet(Service::AM::Frontend::FrontendAppletSet&& set);
|
||||
|
||||
|
||||
@@ -503,10 +503,11 @@ std::vector<Core::Memory::CheatEntry> PatchManager::CreateCheatList(const BuildI
|
||||
for (const auto& subdir : patch_dirs) {
|
||||
if (std::find(disabled.cbegin(), disabled.cend(), subdir->GetName()) == disabled.cend()) {
|
||||
if (auto cheats_dir = FindSubdirectoryCaseless(subdir, "cheats"); cheats_dir != nullptr) {
|
||||
if (auto const res = ReadCheatFileFromFolder(title_id, build_id_, cheats_dir, true))
|
||||
std::copy(res->begin(), res->end(), std::back_inserter(out));
|
||||
if (auto const res = ReadCheatFileFromFolder(title_id, build_id_, cheats_dir, false))
|
||||
if (auto const res = ReadCheatFileFromFolder(title_id, build_id_, cheats_dir, true)) {
|
||||
std::copy(res->begin(), res->end(), std::back_inserter(out));
|
||||
} else if (auto const lower_res = ReadCheatFileFromFolder(title_id, build_id_, cheats_dir, false)) {
|
||||
std::copy(lower_res->begin(), lower_res->end(), std::back_inserter(out));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,11 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <cctype>
|
||||
#include <locale>
|
||||
#include "common/hex_util.h"
|
||||
#include "common/logging.h"
|
||||
#include "common/settings.h"
|
||||
#include "common/swap.h"
|
||||
#include "core/arm/debug.h"
|
||||
#include "core/core.h"
|
||||
@@ -28,11 +31,11 @@ constexpr auto CHEAT_ENGINE_NS = std::chrono::nanoseconds{1000000000 / 12};
|
||||
std::string_view ExtractName(std::size_t& out_name_size, std::string_view data,
|
||||
std::size_t start_index, char match) {
|
||||
auto end_index = start_index;
|
||||
while (data[end_index] != match) {
|
||||
while (end_index < data.size() && data[end_index] != match) {
|
||||
++end_index;
|
||||
if (end_index > data.size()) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
if (end_index == data.size()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
out_name_size = end_index - start_index;
|
||||
@@ -130,6 +133,11 @@ void StandardVmCallbacks::CommandLog(std::string_view data) {
|
||||
data.back() == '\n' ? data.substr(0, data.size() - 1) : data);
|
||||
}
|
||||
|
||||
bool StandardVmCallbacks::IsCommandLogEnabled() const {
|
||||
return Common::Log::IsMessageEnabled(Common::Log::Class::CheatEngine,
|
||||
Common::Log::Level::Debug);
|
||||
}
|
||||
|
||||
bool StandardVmCallbacks::IsAddressInRange(VAddr in) const {
|
||||
if ((in < metadata.main_nso_extents.base ||
|
||||
in >= metadata.main_nso_extents.base + metadata.main_nso_extents.size) &&
|
||||
@@ -166,8 +174,9 @@ std::vector<CheatEntry> TextCheatParser::Parse(std::string_view data) const {
|
||||
|
||||
if (data[i] == '{') {
|
||||
current_entry = 0;
|
||||
out[*current_entry].is_master = true;
|
||||
|
||||
if (out[*current_entry].definition.num_opcodes > 0) {
|
||||
if (!out[*current_entry].definition.opcodes.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -203,32 +212,33 @@ std::vector<CheatEntry> TextCheatParser::Parse(std::string_view data) const {
|
||||
'\0';
|
||||
|
||||
i += name_size + 1;
|
||||
} else if (::isxdigit(data[i])) {
|
||||
if (!current_entry || out[*current_entry].definition.num_opcodes >=
|
||||
out[*current_entry].definition.opcodes.size()) {
|
||||
} else if (std::isxdigit(static_cast<unsigned char>(data[i]))) {
|
||||
if (!current_entry || data.size() - i < 8) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const auto hex = std::string(data.substr(i, 8));
|
||||
if (!std::all_of(hex.begin(), hex.end(), ::isxdigit)) {
|
||||
if (!std::all_of(hex.begin(), hex.end(), [](char value) {
|
||||
return std::isxdigit(static_cast<unsigned char>(value));
|
||||
})) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const auto value = static_cast<u32>(std::strtoul(hex.c_str(), nullptr, 0x10));
|
||||
out[*current_entry].definition.opcodes[out[*current_entry].definition.num_opcodes++] =
|
||||
value;
|
||||
out[*current_entry].definition.opcodes.push_back(value);
|
||||
|
||||
i += 8;
|
||||
i += 7;
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
out[0].enabled = out[0].definition.num_opcodes > 0;
|
||||
out[0].enabled = out[0].is_master && !out[0].definition.opcodes.empty();
|
||||
out[0].cheat_id = 0;
|
||||
|
||||
for (u32 i = 1; i < out.size(); ++i) {
|
||||
out[i].enabled = out[i].definition.num_opcodes > 0;
|
||||
out[i].enabled = Settings::values.enable_cheats_by_default.GetValue() &&
|
||||
!out[i].definition.opcodes.empty();
|
||||
out[i].cheat_id = i;
|
||||
}
|
||||
|
||||
@@ -240,6 +250,10 @@ CheatEngine::CheatEngine(System& system_, std::vector<CheatEntry> cheats_,
|
||||
: vm{std::make_unique<StandardVmCallbacks>(system_, metadata)},
|
||||
cheats(std::move(cheats_)), core_timing{system_.CoreTiming()}, system{system_} {
|
||||
metadata.main_nso_build_id = build_id_;
|
||||
for (u32 i = 0; i < cheats.size(); ++i) {
|
||||
cheats[i].cheat_id = i;
|
||||
cheats[i].enabled = cheats[i].is_master || cheats[i].enabled;
|
||||
}
|
||||
}
|
||||
|
||||
CheatEngine::~CheatEngine() {
|
||||
@@ -286,12 +300,56 @@ void CheatEngine::SetMainMemoryParameters(VAddr main_region_begin, u64 main_regi
|
||||
}
|
||||
|
||||
void CheatEngine::Reload(std::vector<CheatEntry> reload_cheats) {
|
||||
cheats = std::move(reload_cheats);
|
||||
is_pending_reload.exchange(true);
|
||||
{
|
||||
std::scoped_lock lock{cheats_mutex};
|
||||
cheats = std::move(reload_cheats);
|
||||
for (u32 i = 0; i < cheats.size(); ++i) {
|
||||
cheats[i].cheat_id = i;
|
||||
cheats[i].enabled = cheats[i].is_master || cheats[i].enabled;
|
||||
}
|
||||
}
|
||||
is_pending_reload.store(true);
|
||||
}
|
||||
|
||||
std::vector<RuntimeCheatInfo> CheatEngine::GetCheats() const {
|
||||
std::scoped_lock lock{cheats_mutex};
|
||||
std::vector<RuntimeCheatInfo> result;
|
||||
result.reserve(cheats.size());
|
||||
for (const auto& cheat : cheats) {
|
||||
if (cheat.definition.opcodes.empty()) {
|
||||
continue;
|
||||
}
|
||||
result.push_back({
|
||||
.id = cheat.cheat_id,
|
||||
.name = cheat.definition.readable_name.data(),
|
||||
.enabled = cheat.enabled || cheat.is_master,
|
||||
.is_master = cheat.is_master,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool CheatEngine::SetCheatEnabled(u32 cheat_id, bool enabled) {
|
||||
std::scoped_lock lock{cheats_mutex};
|
||||
const auto entry = std::find_if(cheats.begin(), cheats.end(), [cheat_id](const auto& cheat) {
|
||||
return cheat.cheat_id == cheat_id && !cheat.definition.opcodes.empty();
|
||||
});
|
||||
if (entry == cheats.end()) {
|
||||
return false;
|
||||
}
|
||||
if (entry->is_master) {
|
||||
return enabled;
|
||||
}
|
||||
if (entry->enabled != enabled) {
|
||||
entry->enabled = enabled;
|
||||
is_pending_reload.store(true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void CheatEngine::FrameCallback(std::chrono::nanoseconds ns_late) {
|
||||
if (is_pending_reload.exchange(false)) {
|
||||
std::scoped_lock lock{cheats_mutex};
|
||||
vm.LoadProgram(cheats);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
#include "common/common_types.h"
|
||||
#include "core/memory/dmnt_cheat_types.h"
|
||||
@@ -41,6 +42,7 @@ public:
|
||||
void ResumeProcess() override;
|
||||
void DebugLog(u8 id, u64 value) override;
|
||||
void CommandLog(std::string_view data) override;
|
||||
[[nodiscard]] bool IsCommandLogEnabled() const override;
|
||||
|
||||
private:
|
||||
bool IsAddressInRange(VAddr address) const;
|
||||
@@ -81,6 +83,8 @@ public:
|
||||
void SetMainMemoryParameters(VAddr main_region_begin, u64 main_region_size);
|
||||
|
||||
void Reload(std::vector<CheatEntry> reload_cheats);
|
||||
[[nodiscard]] std::vector<RuntimeCheatInfo> GetCheats() const;
|
||||
bool SetCheatEnabled(u32 cheat_id, bool enabled);
|
||||
|
||||
private:
|
||||
void FrameCallback(std::chrono::nanoseconds ns_late);
|
||||
@@ -89,6 +93,7 @@ private:
|
||||
CheatProcessMetadata metadata;
|
||||
|
||||
std::vector<CheatEntry> cheats;
|
||||
mutable std::mutex cheats_mutex;
|
||||
std::atomic_bool is_pending_reload{false};
|
||||
|
||||
std::shared_ptr<Core::Timing::EventType> event;
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "common/common_types.h"
|
||||
|
||||
namespace Core::Memory {
|
||||
@@ -24,14 +30,21 @@ struct CheatProcessMetadata {
|
||||
|
||||
struct CheatDefinition {
|
||||
std::array<char, 0x40> readable_name{};
|
||||
u32 num_opcodes{};
|
||||
std::array<u32, 0x100> opcodes{};
|
||||
std::vector<u32> opcodes;
|
||||
};
|
||||
|
||||
struct CheatEntry {
|
||||
bool enabled{};
|
||||
bool is_master{};
|
||||
u32 cheat_id{};
|
||||
CheatDefinition definition{};
|
||||
};
|
||||
|
||||
struct RuntimeCheatInfo {
|
||||
u32 id{};
|
||||
std::string name;
|
||||
bool enabled{};
|
||||
bool is_master{};
|
||||
};
|
||||
|
||||
} // namespace Core::Memory
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
@@ -233,7 +236,7 @@ bool DmntCheatVm::DecodeNextOpcode(CheatVmOpcode& out) {
|
||||
|
||||
// Helper function for getting instruction dwords.
|
||||
const auto GetNextDword = [&] {
|
||||
if (instruction_ptr >= num_opcodes) {
|
||||
if (instruction_ptr >= program.size()) {
|
||||
valid = false;
|
||||
return static_cast<u32>(0);
|
||||
}
|
||||
@@ -709,20 +712,20 @@ void DmntCheatVm::ResetState() {
|
||||
}
|
||||
|
||||
bool DmntCheatVm::LoadProgram(const std::vector<CheatEntry>& entries) {
|
||||
// Reset opcode count.
|
||||
num_opcodes = 0;
|
||||
program.clear();
|
||||
|
||||
for (std::size_t i = 0; i < entries.size(); i++) {
|
||||
if (entries[i].enabled) {
|
||||
// Bounds check.
|
||||
if (entries[i].definition.num_opcodes + num_opcodes > MaximumProgramOpcodeCount) {
|
||||
num_opcodes = 0;
|
||||
return false;
|
||||
}
|
||||
std::size_t program_size{};
|
||||
for (const auto& entry : entries) {
|
||||
if (entry.enabled || entry.is_master) {
|
||||
program_size += entry.definition.opcodes.size();
|
||||
}
|
||||
}
|
||||
program.reserve(program_size);
|
||||
|
||||
for (std::size_t n = 0; n < entries[i].definition.num_opcodes; n++) {
|
||||
program[num_opcodes++] = entries[i].definition.opcodes[n];
|
||||
}
|
||||
for (const auto& entry : entries) {
|
||||
if (entry.enabled || entry.is_master) {
|
||||
program.insert(program.end(), entry.definition.opcodes.begin(),
|
||||
entry.definition.opcodes.end());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -735,27 +738,35 @@ void DmntCheatVm::Execute(const CheatProcessMetadata& metadata) {
|
||||
// Get Keys down.
|
||||
u64 kDown = callbacks->HidKeysDown();
|
||||
|
||||
callbacks->CommandLog("Started VM execution.");
|
||||
callbacks->CommandLog(fmt::format("Main NSO: {:012X}", metadata.main_nso_extents.base));
|
||||
callbacks->CommandLog(fmt::format("Heap: {:012X}", metadata.main_nso_extents.base));
|
||||
callbacks->CommandLog(fmt::format("Keys Down: {:08X}", static_cast<u32>(kDown & 0x0FFFFFFF)));
|
||||
const bool command_log_enabled = callbacks->IsCommandLogEnabled();
|
||||
if (command_log_enabled) {
|
||||
callbacks->CommandLog("Started VM execution.");
|
||||
callbacks->CommandLog(fmt::format("Main NSO: {:012X}", metadata.main_nso_extents.base));
|
||||
callbacks->CommandLog(fmt::format("Heap: {:012X}", metadata.main_nso_extents.base));
|
||||
callbacks->CommandLog(
|
||||
fmt::format("Keys Down: {:08X}", static_cast<u32>(kDown & 0x0FFFFFFF)));
|
||||
}
|
||||
|
||||
// Clear VM state.
|
||||
ResetState();
|
||||
|
||||
// Loop until program finishes.
|
||||
while (DecodeNextOpcode(cur_opcode)) {
|
||||
callbacks->CommandLog(
|
||||
fmt::format("Instruction Ptr: {:04X}", static_cast<u32>(instruction_ptr)));
|
||||
if (command_log_enabled) {
|
||||
callbacks->CommandLog(
|
||||
fmt::format("Instruction Ptr: {:04X}", static_cast<u32>(instruction_ptr)));
|
||||
|
||||
for (std::size_t i = 0; i < NumRegisters; i++) {
|
||||
callbacks->CommandLog(fmt::format("Registers[{:02X}]: {:016X}", i, registers[i]));
|
||||
}
|
||||
for (std::size_t i = 0; i < NumRegisters; i++) {
|
||||
callbacks->CommandLog(fmt::format("Registers[{:02X}]: {:016X}", i,
|
||||
registers[i]));
|
||||
}
|
||||
|
||||
for (std::size_t i = 0; i < NumRegisters; i++) {
|
||||
callbacks->CommandLog(fmt::format("SavedRegs[{:02X}]: {:016X}", i, saved_values[i]));
|
||||
for (std::size_t i = 0; i < NumRegisters; i++) {
|
||||
callbacks->CommandLog(fmt::format("SavedRegs[{:02X}]: {:016X}", i,
|
||||
saved_values[i]));
|
||||
}
|
||||
LogOpcode(cur_opcode);
|
||||
}
|
||||
LogOpcode(cur_opcode);
|
||||
|
||||
// Increment conditional depth, if relevant.
|
||||
if (cur_opcode.begin_conditional_block) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project
|
||||
@@ -281,9 +281,9 @@ public:
|
||||
|
||||
virtual void DebugLog(u8 id, u64 value) = 0;
|
||||
virtual void CommandLog(std::string_view data) = 0;
|
||||
[[nodiscard]] virtual bool IsCommandLogEnabled() const = 0;
|
||||
};
|
||||
|
||||
static constexpr std::size_t MaximumProgramOpcodeCount = 0x400;
|
||||
static constexpr std::size_t NumRegisters = 0x10;
|
||||
static constexpr std::size_t NumReadableStaticRegisters = 0x80;
|
||||
static constexpr std::size_t NumWritableStaticRegisters = 0x80;
|
||||
@@ -294,7 +294,7 @@ public:
|
||||
~DmntCheatVm();
|
||||
|
||||
std::size_t GetProgramSize() const {
|
||||
return this->num_opcodes;
|
||||
return program.size();
|
||||
}
|
||||
|
||||
bool LoadProgram(const std::vector<CheatEntry>& cheats);
|
||||
@@ -303,11 +303,10 @@ public:
|
||||
private:
|
||||
std::unique_ptr<Callbacks> callbacks;
|
||||
|
||||
std::size_t num_opcodes = 0;
|
||||
std::size_t instruction_ptr = 0;
|
||||
std::size_t condition_depth = 0;
|
||||
bool decode_success = false;
|
||||
std::array<u32, MaximumProgramOpcodeCount> program{};
|
||||
std::vector<u32> program;
|
||||
std::array<u64, NumRegisters> registers{};
|
||||
std::array<u64, NumRegisters> saved_values{};
|
||||
std::array<u64, NumStaticRegisters> static_registers{};
|
||||
|
||||
@@ -86,6 +86,10 @@ std::unique_ptr<TranslationMap> InitializeTranslations(QObject* parent) {
|
||||
tr("Synchronizes CPU core speed with the game's maximum rendering speed to boost FPS "
|
||||
"without affecting game speed (animations, physics, etc.).\n"
|
||||
"Can help reduce stuttering at lower framerates."));
|
||||
INSERT(Settings, enable_cheats_by_default, tr("Enable cheats by default"),
|
||||
tr("Enables regular cheats when a game starts. Master Code entries are always enabled. "
|
||||
"Individual cheats can still be toggled from Cheats in the menu bar while the game is "
|
||||
"running."));
|
||||
|
||||
// Cpu
|
||||
INSERT(Settings, cpu_accuracy, tr("Accuracy:"),
|
||||
|
||||
@@ -91,6 +91,14 @@
|
||||
<addaction name="action_Configure"/>
|
||||
<addaction name="action_Configure_Current_Game"/>
|
||||
</widget>
|
||||
<widget class="QMenu" name="menu_Cheats">
|
||||
<property name="visible">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>&Cheats</string>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QMenu" name="menu_View">
|
||||
<property name="title">
|
||||
<string>&View</string>
|
||||
@@ -241,6 +249,7 @@
|
||||
</widget>
|
||||
<addaction name="menu_File"/>
|
||||
<addaction name="menu_Emulation"/>
|
||||
<addaction name="menu_Cheats"/>
|
||||
<addaction name="menu_View"/>
|
||||
<addaction name="menu_Tools"/>
|
||||
<addaction name="menu_Multiplayer"/>
|
||||
|
||||
@@ -78,10 +78,12 @@
|
||||
#include <QGuiApplication>
|
||||
#include <QInputDialog>
|
||||
#include <QMimeData>
|
||||
#include <QMouseEvent>
|
||||
#include <QPalette>
|
||||
#include <QProgressDialog>
|
||||
#include <QScreen>
|
||||
#include <QShortcut>
|
||||
#include <QSignalBlocker>
|
||||
#include <QStatusBar>
|
||||
#include <QtConcurrentRun>
|
||||
|
||||
@@ -134,6 +136,7 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual
|
||||
#endif
|
||||
|
||||
// Core //
|
||||
#include "core/core.h"
|
||||
#include "core/frontend/applets/general.h"
|
||||
#include "core/frontend/applets/mii_edit.h"
|
||||
#include "core/frontend/applets/software_keyboard.h"
|
||||
@@ -147,6 +150,7 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual
|
||||
#include "core/file_sys/common_funcs.h"
|
||||
#include "core/file_sys/romfs.h"
|
||||
#include "core/file_sys/savedata_factory.h"
|
||||
#include "core/memory/dmnt_cheat_types.h"
|
||||
|
||||
#include "core/tools/renderdoc.h"
|
||||
|
||||
@@ -1434,6 +1438,9 @@ void MainWindow::OnAppFocusStateChanged(Qt::ApplicationState state) {
|
||||
}
|
||||
|
||||
void MainWindow::ConnectWidgetEvents() {
|
||||
ui->menu_Cheats->installEventFilter(this);
|
||||
ui->menu_Cheats->menuAction()->setVisible(false);
|
||||
|
||||
connect(game_list, &GameList::BootGame, this, &MainWindow::BootGameFromList);
|
||||
connect(game_list, &GameList::GameChosen, this, &MainWindow::OnGameListLoadFile);
|
||||
connect(game_list, &GameList::OpenDirectory, this, &MainWindow::OnGameListOpenDirectory);
|
||||
@@ -1468,6 +1475,8 @@ void MainWindow::ConnectWidgetEvents() {
|
||||
// Software Keyboard Applet
|
||||
connect(this, &MainWindow::EmulationStarting, this, &MainWindow::SoftwareKeyboardExit);
|
||||
connect(this, &MainWindow::EmulationStopping, this, &MainWindow::SoftwareKeyboardExit);
|
||||
connect(this, &MainWindow::EmulationStarting, this, &MainWindow::RefreshRuntimeCheats);
|
||||
connect(this, &MainWindow::EmulationStopping, this, &MainWindow::ClearRuntimeCheats);
|
||||
|
||||
connect(&status_bar_update_timer, &QTimer::timeout, this, &MainWindow::UpdateStatusBar);
|
||||
|
||||
@@ -1475,6 +1484,68 @@ void MainWindow::ConnectWidgetEvents() {
|
||||
&MultiplayerState::UpdateThemedIcons);
|
||||
}
|
||||
|
||||
void MainWindow::RefreshRuntimeCheats() {
|
||||
ClearRuntimeCheats();
|
||||
|
||||
const auto cheats = QtCommon::system->GetRuntimeCheats();
|
||||
auto* disable_all = ui->menu_Cheats->addAction(tr("Disable All Cheats"));
|
||||
connect(disable_all, &QAction::triggered, this, [this] {
|
||||
for (auto* action : runtime_cheat_actions) {
|
||||
if (action->isEnabled()) {
|
||||
action->setChecked(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
auto* enable_all = ui->menu_Cheats->addAction(tr("Enable All Cheats"));
|
||||
connect(enable_all, &QAction::triggered, this, [this] {
|
||||
for (auto* action : runtime_cheat_actions) {
|
||||
if (action->isEnabled()) {
|
||||
action->setChecked(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ui->menu_Cheats->addSeparator();
|
||||
runtime_cheat_actions.reserve(cheats.size());
|
||||
for (const auto& cheat : cheats) {
|
||||
const QString name = cheat.name.empty() ? tr("Unnamed Cheat")
|
||||
: QString::fromUtf8(cheat.name);
|
||||
auto* action = ui->menu_Cheats->addAction(name);
|
||||
action->setCheckable(true);
|
||||
action->setChecked(cheat.enabled || cheat.is_master);
|
||||
action->setEnabled(!cheat.is_master);
|
||||
connect(action, &QAction::toggled, this, [action, id = cheat.id](bool enabled) {
|
||||
if (!QtCommon::system->SetCheatEnabled(id, enabled)) {
|
||||
const QSignalBlocker blocker{action};
|
||||
action->setChecked(!enabled);
|
||||
}
|
||||
});
|
||||
runtime_cheat_actions.push_back(action);
|
||||
}
|
||||
|
||||
ui->menu_Cheats->menuAction()->setVisible(!runtime_cheat_actions.empty());
|
||||
}
|
||||
|
||||
void MainWindow::ClearRuntimeCheats() {
|
||||
ui->menu_Cheats->clear();
|
||||
runtime_cheat_actions.clear();
|
||||
ui->menu_Cheats->menuAction()->setVisible(false);
|
||||
}
|
||||
|
||||
bool MainWindow::eventFilter(QObject* watched, QEvent* event) {
|
||||
if (watched == ui->menu_Cheats && event->type() == QEvent::MouseButtonRelease) {
|
||||
const auto* mouse_event = static_cast<QMouseEvent*>(event);
|
||||
auto* action = ui->menu_Cheats->actionAt(mouse_event->pos());
|
||||
if (mouse_event->button() == Qt::LeftButton && action && action->isEnabled() &&
|
||||
action->isCheckable()) {
|
||||
action->trigger();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return QMainWindow::eventFilter(watched, event);
|
||||
}
|
||||
|
||||
void MainWindow::ConnectMenuEvents() {
|
||||
const auto connect_menu = [&]<typename Fn>(QAction* action, const Fn& event_fn) {
|
||||
connect(action, &QAction::triggered, this, event_fn);
|
||||
|
||||
@@ -176,6 +176,9 @@ public:
|
||||
|
||||
std::filesystem::path GetShortcutPath(QtCommon::Game::ShortcutTarget target);
|
||||
|
||||
protected:
|
||||
bool eventFilter(QObject* watched, QEvent* event) override;
|
||||
|
||||
signals:
|
||||
|
||||
/**
|
||||
@@ -264,6 +267,8 @@ private:
|
||||
void ConnectWidgetEvents();
|
||||
void ConnectMenuEvents();
|
||||
void UpdateMenuState();
|
||||
void RefreshRuntimeCheats();
|
||||
void ClearRuntimeCheats();
|
||||
|
||||
void SetupPrepareForSleep();
|
||||
|
||||
@@ -552,6 +557,7 @@ private:
|
||||
QString startup_icon_theme;
|
||||
|
||||
QActionGroup* game_size_actions;
|
||||
std::vector<QAction*> runtime_cheat_actions;
|
||||
|
||||
// Debugger panes
|
||||
ControllerDialog* controller_dialog = nullptr;
|
||||
|
||||
Reference in New Issue
Block a user