From 69665063a9b13dad8fb2b90a64bd8bb0d0e90ea6 Mon Sep 17 00:00:00 2001 From: Soar Qin Date: Fri, 30 Oct 2026 03:47:38 +0100 Subject: [PATCH] [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 Reviewed-by: CamilleLaVey Reviewed-by: John --- docs/user/UsingCheats.md | 17 +++- .../java/org/yuzu/yuzu_emu/NativeLibrary.kt | 10 +++ .../features/settings/model/BooleanSetting.kt | 1 + .../settings/model/view/SettingsItem.kt | 7 ++ .../settings/ui/SettingsFragmentPresenter.kt | 1 + .../yuzu_emu/fragments/EmulationFragment.kt | 49 ++++++++++ src/android/app/src/main/jni/native.cpp | 26 ++++++ .../app/src/main/res/menu/menu_in_game.xml | 4 + .../app/src/main/res/values/strings.xml | 6 ++ src/common/logging.cpp | 4 + src/common/logging.h | 1 + src/common/settings.h | 1 + src/core/core.cpp | 11 +++ src/core/core.h | 3 + src/core/file_sys/patch_manager.cpp | 7 +- src/core/memory/cheat_engine.cpp | 90 +++++++++++++++---- src/core/memory/cheat_engine.h | 5 ++ src/core/memory/dmnt_cheat_types.h | 17 +++- src/core/memory/dmnt_cheat_vm.cpp | 61 +++++++------ src/core/memory/dmnt_cheat_vm.h | 9 +- src/qt_common/config/shared_translation.cpp | 4 + src/yuzu/main.ui | 9 ++ src/yuzu/main_window.cpp | 71 +++++++++++++++ src/yuzu/main_window.h | 6 ++ 24 files changed, 365 insertions(+), 55 deletions(-) diff --git a/docs/user/UsingCheats.md b/docs/user/UsingCheats.md index d6ad7b634a..a89a29d50f 100644 --- a/docs/user/UsingCheats.md +++ b/docs/user/UsingCheats.md @@ -45,12 +45,13 @@ Another option would be to launch the game in Eden and close it - then go into t ***INFO***: Make sure you select the correct directory or it will not work, the screen should show the **cheats** sub-directory. -2. You should now see the cheat appear in the **Add-ons** screen. -3. Launch the game and confirm that the cheat is applied. + +9. You should now see the cheat appear in the **Add-ons** screen. +10. Launch the game and confirm that the cheat is applied. ### Step 2: Multiple Cheats -In order to install multiple cheats, you must repeat the steps above with the new cheat, creating a new directory with the name of the cheat and cheats directory. You **cannot** install multiple cheats with a single file. +In order to install multiple cheats, you must repeat the steps above with the new cheat, creating a new directory with the name of the cheat and cheats directory. Community Member [Ninjistix](https://github.com/Ninjistix) created a utility (Windows or anything that can run Python) that can take a file with multiple cheats and create the files/structure for you with a provided Build ID. To download and run it, see the [GitHub Project](https://github.com/Ninjistix/nxCheat_Splitter) page. @@ -122,7 +123,7 @@ Another option would be to launch the game in Eden and close it - then go into t ### Step 2: Multiple Cheats -In order to install multiple cheats, you must repeat the steps above with the new cheat, creating a new directory with the name of the cheat and cheats directory. You **cannot** install multiple cheats with a single file. +In order to install multiple cheats, you must repeat the steps above with the new cheat, creating a new directory with the name of the cheat and cheats directory. Community Member [Ninjistix](https://github.com/Ninjistix) created a utility (Windows or anything that can run Python) that can take a file with multiple cheats and create the files/structure for you with a provided Build ID. To download and run it, see the [GitHub Project](https://github.com/Ninjistix/nxCheat_Splitter) page. @@ -154,3 +155,11 @@ Cheats are enabled by default, but can be disabled so they don't affect gameplay 3. *Select/Deselect* the name of the cheat you wish to enable/disable. 4. Click **OK** to close the window. 5. Launch the game to confirm the cheat is/is not active. + +### Runtime Cheat Controls + +* While a game is running, open **Cheats** from the menu bar to toggle individual entries from the loaded cheat files. The menu stays open while toggling entries and is hidden when the running game has no cheats. Runtime changes apply to the current game session only. + +* Use **Configure > System > Core > Enable cheats by default** to choose whether regular cheats start enabled. The setting can also be overridden for an individual game from its **System** settings. + +* Master Code entries (`{}` blocks) are always enabled and appear as disabled and checked items in the runtime menu. diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt index 77222884c6..de3477e8b0 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt @@ -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 + external fun setRuntimeCheatEnabled(id: Int, enabled: Boolean): Boolean + /** * Returns the performance stats for the current game */ diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt index 6c33fc7812..2d1909a36f 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt @@ -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"), diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt index f968a0ae18..9e38be12c0 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt @@ -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, diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt index 824a54181e..82d66db16f 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt @@ -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) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt index 3323b3cf9a..65043ec8d2 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt @@ -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(R.id.quick_settings_container) diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index d4f7689934..51d0085c6f 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp @@ -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(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, "", "(ILjava/lang/String;ZZ)V"); + auto result = env->NewObjectArray(static_cast(cheats.size()), runtime_cheat_class, nullptr); + for (jsize i = 0; i < static_cast(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(cheat.id), name, + static_cast(cheat.enabled), + static_cast(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(EmulationSession::GetInstance().System().SetCheatEnabled( + static_cast(id), static_cast(enabled))); +} + jbyteArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getAppletCaptureBuffer(JNIEnv* env, jclass clazz) { using namespace VideoCore::Capture; diff --git a/src/android/app/src/main/res/menu/menu_in_game.xml b/src/android/app/src/main/res/menu/menu_in_game.xml index c1d8147f84..d9ef158d5e 100644 --- a/src/android/app/src/main/res/menu/menu_in_game.xml +++ b/src/android/app/src/main/res/menu/menu_in_game.xml @@ -43,6 +43,10 @@ android:icon="@drawable/ic_nfc" android:title="@string/load_amiibo" /> + Graphics Accuracy level, resolution, shader cache Quick Settings + Cheats + Enable All Cheats + Disable All Cheats + Unnamed Cheat + Enable cheats by default + Enable regular cheats when a game starts Enable Quick Settings Allow access to quick settings menu via swipe and menu button Audio diff --git a/src/common/logging.cpp b/src/common/logging.cpp index ed12d52532..88f92837de 100644 --- a/src/common/logging.cpp +++ b/src/common/logging.cpp @@ -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; diff --git a/src/common/logging.h b/src/common/logging.h index d273eb0c49..8ec1d8a3aa 100644 --- a/src/common/logging.h +++ b/src/common/logging.h @@ -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 diff --git a/src/common/settings.h b/src/common/settings.h index b57bec5c12..e611759429 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -247,6 +247,7 @@ struct Values { SwitchableSetting sync_core_speed{linkage, false, "sync_core_speed", Category::Core, Specialization::Default}; + SwitchableSetting enable_cheats_by_default{linkage, true, "enable_cheats_by_default", Category::Core}; // Cpu SwitchableSetting cpu_backend{linkage, diff --git a/src/core/core.cpp b/src/core/core.cpp index 06543b0833..06c7ee368e 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -786,6 +786,17 @@ void System::RegisterCheatList(const std::vector& list, impl->cheat_engine->SetMainMemoryParameters(main_region_begin, main_region_size); } +std::vector 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)); } diff --git a/src/core/core.h b/src/core/core.h index d88ed64151..22610dabe7 100644 --- a/src/core/core.h +++ b/src/core/core.h @@ -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& list, const std::array& build_id, u64 main_region_begin, u64 main_region_size); + [[nodiscard]] std::vector GetRuntimeCheats() const; + bool SetCheatEnabled(u32 cheat_id, bool enabled); void SetFrontendAppletSet(Service::AM::Frontend::FrontendAppletSet&& set); diff --git a/src/core/file_sys/patch_manager.cpp b/src/core/file_sys/patch_manager.cpp index 4fd7dc6c71..75aff5af9d 100644 --- a/src/core/file_sys/patch_manager.cpp +++ b/src/core/file_sys/patch_manager.cpp @@ -503,10 +503,11 @@ std::vector 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)); + } } } } diff --git a/src/core/memory/cheat_engine.cpp b/src/core/memory/cheat_engine.cpp index e14147f5c9..0af2299b62 100644 --- a/src/core/memory/cheat_engine.cpp +++ b/src/core/memory/cheat_engine.cpp @@ -4,8 +4,11 @@ // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include #include #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 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 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(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(value)); + })) { return {}; } const auto value = static_cast(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 cheats_, : vm{std::make_unique(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 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 CheatEngine::GetCheats() const { + std::scoped_lock lock{cheats_mutex}; + std::vector 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); } diff --git a/src/core/memory/cheat_engine.h b/src/core/memory/cheat_engine.h index abd3abc93f..190eedd3f1 100644 --- a/src/core/memory/cheat_engine.h +++ b/src/core/memory/cheat_engine.h @@ -9,6 +9,7 @@ #include #include #include +#include #include #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 reload_cheats); + [[nodiscard]] std::vector 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 cheats; + mutable std::mutex cheats_mutex; std::atomic_bool is_pending_reload{false}; std::shared_ptr event; diff --git a/src/core/memory/dmnt_cheat_types.h b/src/core/memory/dmnt_cheat_types.h index 64c072d3de..c36b6686ad 100644 --- a/src/core/memory/dmnt_cheat_types.h +++ b/src/core/memory/dmnt_cheat_types.h @@ -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 +#include + #include "common/common_types.h" namespace Core::Memory { @@ -24,14 +30,21 @@ struct CheatProcessMetadata { struct CheatDefinition { std::array readable_name{}; - u32 num_opcodes{}; - std::array opcodes{}; + std::vector 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 diff --git a/src/core/memory/dmnt_cheat_vm.cpp b/src/core/memory/dmnt_cheat_vm.cpp index caceeec4fc..df6fe4b77f 100644 --- a/src/core/memory/dmnt_cheat_vm.cpp +++ b/src/core/memory/dmnt_cheat_vm.cpp @@ -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(0); } @@ -709,20 +712,20 @@ void DmntCheatVm::ResetState() { } bool DmntCheatVm::LoadProgram(const std::vector& 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(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(kDown & 0x0FFFFFFF))); + } // Clear VM state. ResetState(); // Loop until program finishes. while (DecodeNextOpcode(cur_opcode)) { - callbacks->CommandLog( - fmt::format("Instruction Ptr: {:04X}", static_cast(instruction_ptr))); + if (command_log_enabled) { + callbacks->CommandLog( + fmt::format("Instruction Ptr: {:04X}", static_cast(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) { diff --git a/src/core/memory/dmnt_cheat_vm.h b/src/core/memory/dmnt_cheat_vm.h index de5e81add2..f7c10f9130 100644 --- a/src/core/memory/dmnt_cheat_vm.h +++ b/src/core/memory/dmnt_cheat_vm.h @@ -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& cheats); @@ -303,11 +303,10 @@ public: private: std::unique_ptr 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 program{}; + std::vector program; std::array registers{}; std::array saved_values{}; std::array static_registers{}; diff --git a/src/qt_common/config/shared_translation.cpp b/src/qt_common/config/shared_translation.cpp index bb92d896f8..e9da2fa923 100644 --- a/src/qt_common/config/shared_translation.cpp +++ b/src/qt_common/config/shared_translation.cpp @@ -86,6 +86,10 @@ std::unique_ptr 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:"), diff --git a/src/yuzu/main.ui b/src/yuzu/main.ui index 92b725fa11..d79fcbe396 100644 --- a/src/yuzu/main.ui +++ b/src/yuzu/main.ui @@ -91,6 +91,14 @@ + + + false + + + &Cheats + + &View @@ -241,6 +249,7 @@ + diff --git a/src/yuzu/main_window.cpp b/src/yuzu/main_window.cpp index ad76e5b2db..217f8d0356 100644 --- a/src/yuzu/main_window.cpp +++ b/src/yuzu/main_window.cpp @@ -78,10 +78,12 @@ #include #include #include +#include #include #include #include #include +#include #include #include @@ -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(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 = [&](QAction* action, const Fn& event_fn) { connect(action, &QAction::triggered, this, event_fn); diff --git a/src/yuzu/main_window.h b/src/yuzu/main_window.h index 8aedeec721..b807130812 100644 --- a/src/yuzu/main_window.h +++ b/src/yuzu/main_window.h @@ -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 runtime_cheat_actions; // Debugger panes ControllerDialog* controller_dialog = nullptr;