mirror of
https://git.eden-emu.dev/eden-emu/eden.git
synced 2026-10-30 03:10:19 +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:
@@ -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.
|
||||
|
||||
</aside>
|
||||
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.
|
||||
|
||||
@@ -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