[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:
Soar Qin
2026-10-30 03:47:38 +01:00
committed by crueter
parent b4d5760014
commit 69665063a9
24 changed files with 365 additions and 55 deletions
+11
View File
@@ -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));
}
+3
View File
@@ -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);
+4 -3
View File
@@ -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));
}
}
}
}
+74 -16
View File
@@ -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);
}
+5
View File
@@ -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;
+15 -2
View File
@@ -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
+36 -25
View File
@@ -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) {
+4 -5
View File
@@ -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{};