diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AddonAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AddonAdapter.kt index 2be91ba46a..47ccca556c 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AddonAdapter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AddonAdapter.kt @@ -6,10 +6,15 @@ package org.yuzu.yuzu_emu.adapters +import android.view.Gravity import android.view.LayoutInflater +import android.view.View import android.view.ViewGroup +import androidx.constraintlayout.widget.ConstraintLayout +import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.databinding.ListItemAddonBinding import org.yuzu.yuzu_emu.model.Patch +import org.yuzu.yuzu_emu.model.PatchType import org.yuzu.yuzu_emu.model.AddonViewModel import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder @@ -27,18 +32,94 @@ class AddonAdapter(val addonViewModel: AddonViewModel) : binding.addonSwitch.performClick() } binding.title.text = model.name - binding.version.text = model.version binding.addonSwitch.isChecked = model.enabled binding.addonSwitch.setOnCheckedChangeListener { _, checked -> model.enabled = checked } - val deleteAction = { - addonViewModel.setAddonToDelete(model) + val isCheat = model.isCheat() + val indentPx = if (isCheat) { + binding.root.context.resources.getDimensionPixelSize(R.dimen.spacing_large) + } else { + 0 + } + (binding.addonCard.layoutParams as? ViewGroup.MarginLayoutParams)?.let { + it.marginStart = indentPx + binding.addonCard.layoutParams = it + } + + if (isCheat) { + binding.version.visibility = View.GONE + binding.deleteCard.visibility = View.GONE + binding.buttonDelete.visibility = View.GONE + + binding.addonSwitch.scaleX = 0.7f + binding.addonSwitch.scaleY = 0.7f + + val compactPaddingVertical = binding.root.context.resources.getDimensionPixelSize(R.dimen.spacing_small) + binding.root.setPadding( + binding.root.paddingLeft, + compactPaddingVertical / 2, + binding.root.paddingRight, + compactPaddingVertical / 2 + ) + + val innerLayout = binding.addonCard.getChildAt(0) as? ViewGroup + innerLayout?.let { + val leftPadding = binding.root.context.resources.getDimensionPixelSize(R.dimen.spacing_large) + val smallPadding = binding.root.context.resources.getDimensionPixelSize(R.dimen.spacing_med) + it.setPadding(leftPadding, smallPadding, smallPadding, smallPadding) + } + + binding.title.textSize = 14f + binding.title.gravity = Gravity.CENTER_VERTICAL + + (binding.title.layoutParams as? ConstraintLayout.LayoutParams)?.let { params -> + params.topToTop = ConstraintLayout.LayoutParams.PARENT_ID + params.bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID + params.topMargin = 0 + binding.title.layoutParams = params + } + } else { + binding.version.visibility = View.VISIBLE + binding.version.text = model.version + binding.deleteCard.visibility = View.VISIBLE + binding.buttonDelete.visibility = View.VISIBLE + + binding.addonSwitch.scaleX = 1.0f + binding.addonSwitch.scaleY = 1.0f + + val normalPadding = binding.root.context.resources.getDimensionPixelSize(R.dimen.spacing_med) + binding.root.setPadding( + binding.root.paddingLeft, + normalPadding, + binding.root.paddingRight, + normalPadding + ) + + val innerLayout = binding.addonCard.getChildAt(0) as? ViewGroup + innerLayout?.let { + val normalInnerPadding = binding.root.context.resources.getDimensionPixelSize(R.dimen.spacing_medlarge) + it.setPadding(normalInnerPadding, normalInnerPadding, normalInnerPadding, normalInnerPadding) + } + + binding.title.textSize = 16f + binding.title.gravity = Gravity.START + + (binding.title.layoutParams as? ConstraintLayout.LayoutParams)?.let { params -> + params.topToTop = ConstraintLayout.LayoutParams.PARENT_ID + params.bottomToBottom = ConstraintLayout.LayoutParams.UNSET + params.topMargin = 0 + binding.title.layoutParams = params + } + + val deleteAction = { + addonViewModel.setAddonToDelete(model) + } + binding.deleteCard.setOnClickListener { deleteAction() } + binding.buttonDelete.setOnClickListener { deleteAction() } } - binding.deleteCard.setOnClickListener { deleteAction() } - binding.buttonDelete.setOnClickListener { deleteAction() } } } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/AddonViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/AddonViewModel.kt index b9c8e49ca4..90938870ca 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/AddonViewModel.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/AddonViewModel.kt @@ -47,8 +47,7 @@ class AddonViewModel : ViewModel() { NativeLibrary.getPatchesForFile(game!!.path, game!!.programId) ?: emptyArray() ).toMutableList() - patchList.sortBy { it.name } - _patchList.value = patchList + _patchList.value = sortPatchesWithCheatsGrouped(patchList) isRefreshing.set(false) } } @@ -63,6 +62,7 @@ class AddonViewModel : ViewModel() { PatchType.Update -> NativeLibrary.removeUpdate(patch.programId) PatchType.DLC -> NativeLibrary.removeDLC(patch.programId) PatchType.Mod -> NativeLibrary.removeMod(patch.programId, patch.name) + PatchType.Cheat -> {} } refreshAddons() } @@ -78,7 +78,7 @@ class AddonViewModel : ViewModel() { if (it.enabled) { null } else { - it.name + it.getStorageKey() // Use storage key for proper cheat identification } }.toTypedArray() ) @@ -94,4 +94,28 @@ class AddonViewModel : ViewModel() { fun showModNoticeDialog(show: Boolean) { _showModNoticeDialog.value = show } + + private fun sortPatchesWithCheatsGrouped(patches: MutableList): MutableList { + val individualCheats = patches.filter { it.isCheat() } + val nonCheats = patches.filter { !it.isCheat() }.sortedBy { it.name } + + val cheatsByParent = individualCheats.groupBy { it.parentName } + + val result = mutableListOf() + for (patch in nonCheats) { + result.add(patch) + cheatsByParent[patch.name]?.sortedBy { it.name }?.let { childCheats -> + result.addAll(childCheats) + } + } + + val knownParents = nonCheats.map { it.name }.toSet() + for ((parentName, orphanCheats) in cheatsByParent) { + if (parentName !in knownParents) { + result.addAll(orphanCheats.sortedBy { it.name }) + } + } + + return result + } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Patch.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Patch.kt index 25cb9e3654..9c2ad80eae 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Patch.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Patch.kt @@ -12,5 +12,24 @@ data class Patch( val version: String, val type: Int, val programId: String, - val titleId: String -) + val titleId: String, + val parentName: String = "" // For cheats: name of the mod folder containing them +) { + /** + * Returns the storage key used for saving enabled/disabled state. + * For cheats with a parent, returns "ParentName::CheatName". + */ + fun getStorageKey(): String { + return if (parentName.isNotEmpty()) { + "$parentName::$name" + } else { + name + } + } + + /** + * Returns true if this patch is an individual cheat entry (not a cheat mod). + * Individual cheats have type=Cheat and a parent mod name. + */ + fun isCheat(): Boolean = type == PatchType.Cheat.int && parentName.isNotEmpty() +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/PatchType.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/PatchType.kt index e9a54162b0..271d3b869b 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/PatchType.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/PatchType.kt @@ -6,7 +6,8 @@ package org.yuzu.yuzu_emu.model enum class PatchType(val int: Int) { Update(0), DLC(1), - Mod(2); + Mod(2), + Cheat(3); companion object { fun from(int: Int): PatchType = entries.firstOrNull { it.int == int } ?: Update diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index f28f4a9b7b..a966ec454f 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp @@ -1298,7 +1298,10 @@ jobjectArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getPatchesForFile(JNIEnv* env FileSys::VirtualFile update_raw; loader->ReadUpdateRaw(update_raw); - auto patches = pm.GetPatches(update_raw); + // Get build ID for individual cheat enumeration + const auto build_id = pm.GetBuildID(update_raw); + + auto patches = pm.GetPatches(update_raw, build_id); jobjectArray jpatchArray = env->NewObjectArray(patches.size(), Common::Android::GetPatchClass(), nullptr); int i = 0; @@ -1308,7 +1311,8 @@ jobjectArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getPatchesForFile(JNIEnv* env Common::Android::ToJString(env, patch.name), Common::Android::ToJString(env, patch.version), static_cast(patch.type), Common::Android::ToJString(env, std::to_string(patch.program_id)), - Common::Android::ToJString(env, std::to_string(patch.title_id))); + Common::Android::ToJString(env, std::to_string(patch.title_id)), + Common::Android::ToJString(env, patch.parent_name)); env->SetObjectArrayElement(jpatchArray, i, jpatch); ++i; } diff --git a/src/common/android/id_cache.cpp b/src/common/android/id_cache.cpp index 1198833996..4422c57178 100644 --- a/src/common/android/id_cache.cpp +++ b/src/common/android/id_cache.cpp @@ -515,7 +515,7 @@ namespace Common::Android { s_patch_class = reinterpret_cast(env->NewGlobalRef(patch_class)); s_patch_constructor = env->GetMethodID( patch_class, "", - "(ZLjava/lang/String;Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;)V"); + "(ZLjava/lang/String;Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;)V"); s_patch_enabled_field = env->GetFieldID(patch_class, "enabled", "Z"); s_patch_name_field = env->GetFieldID(patch_class, "name", "Ljava/lang/String;"); s_patch_version_field = env->GetFieldID(patch_class, "version", "Ljava/lang/String;"); diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index a961eff8bf..7f5fe3258a 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -611,6 +611,18 @@ add_library(core STATIC hle/service/caps/caps_u.h hle/service/cmif_serialization.h hle/service/cmif_types.h + hle/service/dmnt/cheat_interface.cpp + hle/service/dmnt/cheat_interface.h + hle/service/dmnt/cheat_parser.cpp + hle/service/dmnt/cheat_parser.h + hle/service/dmnt/cheat_process_manager.cpp + hle/service/dmnt/cheat_process_manager.h + hle/service/dmnt/cheat_virtual_machine.cpp + hle/service/dmnt/cheat_virtual_machine.h + hle/service/dmnt/dmnt.cpp + hle/service/dmnt/dmnt.h + hle/service/dmnt/dmnt_results.h + hle/service/dmnt/dmnt_types.h hle/service/erpt/erpt.cpp hle/service/erpt/erpt.h hle/service/es/es.cpp @@ -1145,11 +1157,6 @@ add_library(core STATIC loader/xci.h memory.cpp memory.h - memory/cheat_engine.cpp - memory/cheat_engine.h - memory/dmnt_cheat_types.h - memory/dmnt_cheat_vm.cpp - memory/dmnt_cheat_vm.h perf_stats.cpp perf_stats.h reporter.cpp diff --git a/src/core/core.cpp b/src/core/core.cpp index bf97184f8f..257a35f6ca 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -52,12 +52,12 @@ #include "core/internal_network/network.h" #include "core/loader/loader.h" #include "core/memory.h" -#include "core/memory/cheat_engine.h" #include "core/perf_stats.h" #include "core/reporter.h" #include "core/tools/freezer.h" #include "core/tools/renderdoc.h" #include "hid_core/hid_core.h" +#include "hle/service/dmnt/cheat_process_manager.h" #include "network/network.h" #include "video_core/host1x/host1x.h" #include "video_core/renderer_base.h" @@ -277,9 +277,18 @@ struct System::Impl { audio_core = std::make_unique(system); service_manager = std::make_shared(kernel); + + // Create cheat_manager BEFORE services, as DMNT::LoopProcess needs it + cheat_manager = std::make_unique(system); + services = std::make_unique(service_manager, system, stop_event.get_token()); + // Apply any pending cheats that were registered before cheat_manager was initialized + if (pending_cheats.has_pending) { + ApplyPendingCheats(system); + } + is_powered_on = true; exit_locked = false; exit_requested = false; @@ -343,11 +352,6 @@ struct System::Impl { return init_result; } - // Initialize cheat engine - if (cheat_engine) { - cheat_engine->Initialize(); - } - // Register with applet manager // All threads are started, begin main process execution, now that we're in the clear applet_manager.CreateAndInsertByFrontendAppletParameters(std::move(process), params); @@ -415,7 +419,6 @@ struct System::Impl { services.reset(); service_manager.reset(); fs_controller.Reset(); - cheat_engine.reset(); core_timing.ClearPendingEvents(); app_loader.reset(); audio_core.reset(); @@ -491,7 +494,6 @@ struct System::Impl { bool nvdec_active{}; Reporter reporter; - std::unique_ptr cheat_engine; std::unique_ptr memory_freezer; std::array build_id{}; @@ -520,6 +522,18 @@ struct System::Impl { /// Debugger std::unique_ptr debugger; + /// Cheat Manager (DMNT) + std::unique_ptr cheat_manager; + + /// Pending cheats to register after cheat_manager is initialized + struct PendingCheats { + std::vector list; + std::array build_id{}; + u64 main_region_begin{}; + u64 main_region_size{}; + bool has_pending{false}; + } pending_cheats; + SystemResultStatus status = SystemResultStatus::Success; std::string status_details = ""; @@ -555,6 +569,61 @@ struct System::Impl { general_channel_event = std::make_unique(*general_channel_context); general_channel_initialized = true; } + + void ApplyPendingCheats(System& system) { + if (!pending_cheats.has_pending || !cheat_manager) { + return; + } + + LOG_DEBUG(Core, "Applying {} pending cheats", pending_cheats.list.size()); + + const auto result = cheat_manager->AttachToApplicationProcess( + pending_cheats.build_id, pending_cheats.main_region_begin, + pending_cheats.main_region_size); + + if (result.IsError()) { + LOG_WARNING(Core, "Failed to attach cheat process: result={}", result.raw); + pending_cheats = {}; + return; + } + + LOG_DEBUG(Core, "Cheat process attached successfully"); + + for (const auto& entry : pending_cheats.list) { + if (entry.cheat_id == 0 && entry.definition.num_opcodes != 0) { + LOG_DEBUG(Core, "Setting master cheat '{}' with {} opcodes", + entry.definition.readable_name.data(), entry.definition.num_opcodes); + const auto set_result = cheat_manager->SetMasterCheat(entry.definition); + if (set_result.IsError()) { + LOG_WARNING(Core, "Failed to set master cheat: result={}", set_result.raw); + } + break; + } + } + + // Add normal cheats (cheat_id != 0) + for (const auto& entry : pending_cheats.list) { + if (entry.cheat_id == 0 || entry.definition.num_opcodes == 0) { + continue; + } + + u32 assigned_id = 0; + LOG_DEBUG(Core, "Adding cheat '{}' (enabled={}, {} opcodes)", + entry.definition.readable_name.data(), entry.enabled, + entry.definition.num_opcodes); + const auto add_result = cheat_manager->AddCheat(assigned_id, entry.enabled, + entry.definition); + if (add_result.IsError()) { + LOG_WARNING(Core, + "Failed to add cheat (original_id={} enabled={} name='{}'): result={}", + entry.cheat_id, entry.enabled, + entry.definition.readable_name.data(), add_result.raw); + } + } + + // Clear pending cheats + pending_cheats = {}; + } }; System::System() : impl{std::make_unique(*this)} {} @@ -800,11 +869,61 @@ FileSys::VirtualFilesystem System::GetFilesystem() const { return impl->virtual_filesystem; } -void System::RegisterCheatList(const std::vector& list, +void System::RegisterCheatList(const std::vector& list, const std::array& build_id, u64 main_region_begin, u64 main_region_size) { - impl->cheat_engine = std::make_unique(*this, list, build_id); - impl->cheat_engine->SetMainMemoryParameters(main_region_begin, main_region_size); + // If cheat_manager is not yet initialized, cache the cheats for later + if (!impl->cheat_manager) { + impl->pending_cheats.list = list; + impl->pending_cheats.build_id = build_id; + impl->pending_cheats.main_region_begin = main_region_begin; + impl->pending_cheats.main_region_size = main_region_size; + impl->pending_cheats.has_pending = true; + LOG_INFO(Core, "Cached {} cheats for later registration", list.size()); + return; + } + + // Attach cheat process to the current application process + const auto result = impl->cheat_manager->AttachToApplicationProcess(build_id, main_region_begin, + main_region_size); + if (result.IsError()) { + LOG_WARNING(Core, "Failed to attach cheat process: result={}", result.raw); + return; + } + + // Empty list: nothing more to do + if (list.empty()) { + return; + } + + // Set master cheat if present (cheat_id == 0) + for (const auto& entry : list) { + if (entry.cheat_id == 0 && entry.definition.num_opcodes != 0) { + const auto set_result = impl->cheat_manager->SetMasterCheat(entry.definition); + if (set_result.IsError()) { + LOG_WARNING(Core, "Failed to set master cheat: result={}", set_result.raw); + } + // Only one master cheat allowed + break; + } + } + + // Add normal cheats (cheat_id != 0) + for (const auto& entry : list) { + if (entry.cheat_id == 0 || entry.definition.num_opcodes == 0) { + continue; + } + + u32 assigned_id = 0; + const auto add_result = impl->cheat_manager->AddCheat(assigned_id, entry.enabled, + entry.definition); + if (add_result.IsError()) { + LOG_WARNING(Core, + "Failed to add cheat (original_id={} enabled={} name='{}'): result={}", + entry.cheat_id, entry.enabled, + entry.definition.readable_name.data(), add_result.raw); + } + } } void System::SetFrontendAppletSet(Service::AM::Frontend::FrontendAppletSet&& set) { @@ -948,6 +1067,15 @@ Tools::RenderdocAPI& System::GetRenderdocAPI() { return *impl->renderdoc_api; } +Service::DMNT::CheatProcessManager& System::GetCheatManager() +{ + return *impl->cheat_manager; +} + + const Service::DMNT::CheatProcessManager& System::GetCheatManager() const { + return *impl->cheat_manager; +} + void System::RunServer(std::unique_ptr&& server_manager) { return impl->kernel.RunServer(std::move(server_manager)); } diff --git a/src/core/core.h b/src/core/core.h index 60bf73d4e1..7fd8bcdb83 100644 --- a/src/core/core.h +++ b/src/core/core.h @@ -17,6 +17,7 @@ #include "common/common_types.h" #include "core/file_sys/vfs/vfs_types.h" +#include "core/hle/service/dmnt/dmnt_types.h" #include "core/hle/service/os/event.h" #include "core/hle/service/kernel_helpers.h" @@ -45,10 +46,14 @@ enum class ResultStatus : u16; } // namespace Loader namespace Core::Memory { -struct CheatEntry; class Memory; } // namespace Core::Memory +namespace Service::DMNT { + class CheatProcessManager; + struct CheatEntry; +} + namespace Service { namespace Account { @@ -339,7 +344,7 @@ public: [[nodiscard]] FileSys::VirtualFilesystem GetFilesystem() const; - void RegisterCheatList(const std::vector& list, + void RegisterCheatList(const std::vector& list, const std::array& build_id, u64 main_region_begin, u64 main_region_size); @@ -383,6 +388,9 @@ public: [[nodiscard]] Tools::RenderdocAPI& GetRenderdocAPI(); + [[nodiscard]] Service::DMNT::CheatProcessManager& GetCheatManager(); + [[nodiscard]] const Service::DMNT::CheatProcessManager& GetCheatManager() const; + void SetExitLocked(bool locked); bool GetExitLocked() const; diff --git a/src/core/file_sys/patch_manager.cpp b/src/core/file_sys/patch_manager.cpp index a28fdf9056..9ad48bead3 100644 --- a/src/core/file_sys/patch_manager.cpp +++ b/src/core/file_sys/patch_manager.cpp @@ -27,12 +27,13 @@ #include "core/file_sys/vfs/vfs_cached.h" #include "core/file_sys/vfs/vfs_layered.h" #include "core/file_sys/vfs/vfs_vector.h" +#include "core/hle/service/dmnt/cheat_parser.h" +#include "core/hle/service/dmnt/dmnt_types.h" #include "core/hle/service/filesystem/filesystem.h" #include "core/hle/service/ns/language.h" #include "core/hle/service/set/settings_server.h" #include "core/loader/loader.h" #include "core/loader/nso.h" -#include "core/memory/cheat_engine.h" namespace FileSys { namespace { @@ -64,16 +65,15 @@ std::string FormatTitleVersion(u32 version, return fmt::format("v{}.{}.{}", bytes[3], bytes[2], bytes[1]); } -// Returns a directory with name matching name case-insensitive. Returns nullptr if directory -// doesn't have a directory with name. -VirtualDir FindSubdirectoryCaseless(const VirtualDir dir, std::string_view name) { +// Returns a directory with name matching case-insensitively. +// Returns nullptr if directory doesn't contain a subdirectory with the given name. +VirtualDir FindSubdirectoryCaseless(const VirtualDir& dir, std::string_view name) { #ifdef _WIN32 return dir->GetSubdirectory(name); #else - const auto subdirs = dir->GetSubdirectories(); - for (const auto& subdir : subdirs) { - std::string dir_name = Common::ToLower(subdir->GetName()); - if (dir_name == name) { + const auto target = Common::ToLower(std::string(name)); + for (const auto& subdir : dir->GetSubdirectories()) { + if (Common::ToLower(subdir->GetName()) == target) { return subdir; } } @@ -82,36 +82,35 @@ VirtualDir FindSubdirectoryCaseless(const VirtualDir dir, std::string_view name) #endif } -std::optional> ReadCheatFileFromFolder( +std::optional> ReadCheatFileFromFolder( u64 title_id, const PatchManager::BuildID& build_id_, const VirtualDir& base_path, bool upper) { + const auto build_id_raw = Common::HexToString(build_id_, upper); - const auto build_id = build_id_raw.substr(0, sizeof(u64) * 2); + const auto build_id = build_id_raw.substr(0, std::min(build_id_raw.size(), sizeof(u64) * 2)); const auto file = base_path->GetFile(fmt::format("{}.txt", build_id)); if (file == nullptr) { - LOG_INFO(Common_Filesystem, "No cheats file found for title_id={:016X}, build_id={}", - title_id, build_id); + LOG_DEBUG(Common_Filesystem, "No cheats file found for title_id={:016X}, build_id={}", + title_id, build_id); return std::nullopt; } std::vector data(file->GetSize()); if (file->Read(data.data(), data.size()) != data.size()) { - LOG_INFO(Common_Filesystem, "Failed to read cheats file for title_id={:016X}, build_id={}", - title_id, build_id); + LOG_WARNING(Common_Filesystem, "Failed to read cheats file for title_id={:016X}, build_id={}", + title_id, build_id); return std::nullopt; } - const Core::Memory::TextCheatParser parser; + const Service::DMNT::CheatParser parser; return parser.Parse(std::string_view(reinterpret_cast(data.data()), data.size())); } void AppendCommaIfNotEmpty(std::string& to, std::string_view with) { - if (to.empty()) { - to += with; - } else { + if (!to.empty()) { to += ", "; - to += with; } + to += with; } bool IsDirValidAndNonEmpty(const VirtualDir& dir) { @@ -316,7 +315,7 @@ bool PatchManager::HasNSOPatch(const BuildID& build_id_, std::string_view name) return !CollectPatches(patch_dirs, build_id).empty(); } -std::vector PatchManager::CreateCheatList(const BuildID& build_id_) const { +std::vector PatchManager::CreateCheatList(const BuildID& build_id_) const { const auto load_dir = fs_controller.GetModificationLoadRoot(title_id); if (load_dir == nullptr) { LOG_ERROR(Loader, "Cannot load mods for invalid title_id={:016X}", title_id); @@ -325,36 +324,71 @@ std::vector PatchManager::CreateCheatList(const BuildI const auto& disabled = Settings::values.disabled_addons[title_id]; auto patch_dirs = load_dir->GetSubdirectories(); - std::sort(patch_dirs.begin(), patch_dirs.end(), [](auto const& l, auto const& r) { return l->GetName() < r->GetName(); }); + std::sort(patch_dirs.begin(), patch_dirs.end(), + [](const VirtualDir& l, const VirtualDir& r) { return l->GetName() < r->GetName(); }); - // / / cheats / .txt - std::vector out; + std::vector out; + + // Load cheats from: //cheats/.txt 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)) - std::copy(res->begin(), res->end(), std::back_inserter(out)); + const auto mod_name = subdir->GetName(); + + // Skip entirely disabled mods + if (std::find(disabled.cbegin(), disabled.cend(), mod_name) != disabled.cend()) { + continue; + } + + auto cheats_dir = FindSubdirectoryCaseless(subdir, "cheats"); + if (cheats_dir == nullptr) { + continue; + } + + // Try uppercase build_id first, then lowercase + std::optional> cheat_entries; + if (auto res = ReadCheatFileFromFolder(title_id, build_id_, cheats_dir, true)) { + cheat_entries = std::move(res); + } else if (auto res_lower = ReadCheatFileFromFolder(title_id, build_id_, cheats_dir, false)) { + cheat_entries = std::move(res_lower); + } + + if (cheat_entries) { + for (auto& entry : *cheat_entries) { + // Check if this individual cheat is disabled + const std::string cheat_name = entry.definition.readable_name.data(); + const std::string cheat_key = mod_name + "::" + cheat_name; + + if (std::find(disabled.cbegin(), disabled.cend(), cheat_key) != disabled.cend()) { + // Individual cheat is disabled - mark it as disabled but still include it + entry.enabled = false; + } + + out.push_back(entry); } } } - // Uncareless user-friendly loading of patches (must start with 'cheat_') - // / .txt - auto const patch_files = load_dir->GetFiles(); - for (auto const& f : patch_files) { - auto const name = f->GetName(); - if (name.starts_with("cheat_") && std::find(disabled.cbegin(), disabled.cend(), name) == disabled.cend()) { - std::vector data(f->GetSize()); - if (f->Read(data.data(), data.size()) == data.size()) { - const Core::Memory::TextCheatParser parser; - auto const res = parser.Parse(std::string_view(reinterpret_cast(data.data()), data.size())); - std::copy(res.begin(), res.end(), std::back_inserter(out)); - } else { - LOG_INFO(Common_Filesystem, "Failed to read cheats file for title_id={:016X}", title_id); - } + + // User-friendly cheat loading from: /cheat_*.txt + for (const auto& file : load_dir->GetFiles()) { + const auto& name = file->GetName(); + if (!name.starts_with("cheat_")) { + continue; + } + if (std::find(disabled.cbegin(), disabled.cend(), name) != disabled.cend()) { + continue; + } + + std::vector data(file->GetSize()); + if (file->Read(data.data(), data.size()) != static_cast(data.size())) { + LOG_WARNING(Common_Filesystem, "Failed to read cheat file '{}' for title_id={:016X}", + name, title_id); + continue; } + + const Service::DMNT::CheatParser parser; + auto entries = parser.Parse(std::string_view(reinterpret_cast(data.data()), data.size())); + out.insert(out.end(), entries.begin(), entries.end()); } + return out; } @@ -481,7 +515,53 @@ VirtualFile PatchManager::PatchRomFS(const NCA* base_nca, VirtualFile base_romfs return romfs; } -std::vector PatchManager::GetPatches(VirtualFile update_raw) const { +PatchManager::BuildID PatchManager::GetBuildID(VirtualFile update_raw) const { + BuildID build_id{}; + + // Get the base NCA + const auto base_nca = content_provider.GetEntry(title_id, ContentRecordType::Program); + if (base_nca == nullptr) { + return build_id; + } + + // Try to get ExeFS from update first, then base + VirtualDir exefs; + const auto update_tid = GetUpdateTitleID(title_id); + const auto update = content_provider.GetEntry(update_tid, ContentRecordType::Program); + + if (update != nullptr && update->GetExeFS() != nullptr) { + exefs = update->GetExeFS(); + } else if (update_raw != nullptr) { + const auto new_nca = std::make_shared(update_raw, base_nca.get()); + if (new_nca->GetStatus() == Loader::ResultStatus::Success && + new_nca->GetExeFS() != nullptr) { + exefs = new_nca->GetExeFS(); + } + } + + if (exefs == nullptr) { + exefs = base_nca->GetExeFS(); + } + + if (exefs == nullptr) { + return build_id; + } + + // Try to read the main NSO header + const auto main_file = exefs->GetFile("main"); + if (main_file == nullptr || main_file->GetSize() < sizeof(Loader::NSOHeader)) { + return build_id; + } + + Loader::NSOHeader header{}; + if (main_file->Read(reinterpret_cast(&header), sizeof(header)) == sizeof(header)) { + build_id = header.build_id; + } + + return build_id; +} + +std::vector PatchManager::GetPatches(VirtualFile update_raw, const BuildID& build_id) const { if (title_id == 0) { return {}; } @@ -502,7 +582,8 @@ std::vector PatchManager::GetPatches(VirtualFile update_raw) const { .version = "", .type = PatchType::Update, .program_id = title_id, - .title_id = title_id}; + .title_id = title_id, + .parent_name = ""}; if (nacp != nullptr) { update_patch.version = nacp->GetVersionString(); @@ -522,11 +603,15 @@ std::vector PatchManager::GetPatches(VirtualFile update_raw) const { } } + // Check if we have a valid build_id for cheat enumeration + const bool has_build_id = std::any_of(build_id.begin(), build_id.end(), [](u8 b) { return b != 0; }); + // General Mods (LayeredFS and IPS) const auto mod_dir = fs_controller.GetModificationLoadRoot(title_id); if (mod_dir != nullptr) { for (const auto& mod : mod_dir->GetSubdirectories()) { std::string types; + bool has_cheats = false; const auto exefs_dir = FindSubdirectoryCaseless(mod, "exefs"); if (IsDirValidAndNonEmpty(exefs_dir)) { @@ -555,8 +640,12 @@ std::vector PatchManager::GetPatches(VirtualFile update_raw) const { if (IsDirValidAndNonEmpty(FindSubdirectoryCaseless(mod, "romfs")) || IsDirValidAndNonEmpty(FindSubdirectoryCaseless(mod, "romfslite"))) AppendCommaIfNotEmpty(types, "LayeredFS"); - if (IsDirValidAndNonEmpty(FindSubdirectoryCaseless(mod, "cheats"))) + + const auto cheats_dir = FindSubdirectoryCaseless(mod, "cheats"); + if (IsDirValidAndNonEmpty(cheats_dir)) { + has_cheats = true; AppendCommaIfNotEmpty(types, "Cheats"); + } if (types.empty()) continue; @@ -568,7 +657,46 @@ std::vector PatchManager::GetPatches(VirtualFile update_raw) const { .version = types, .type = PatchType::Mod, .program_id = title_id, - .title_id = title_id}); + .title_id = title_id, + .parent_name = ""}); + + // Add individual cheats as sub-entries if we have a build_id + if (has_cheats && has_build_id && !mod_disabled) { + // Try to read cheat file (uppercase first, then lowercase) + std::optional> cheat_entries; + if (auto res = ReadCheatFileFromFolder(title_id, build_id, cheats_dir, true)) { + cheat_entries = std::move(res); + } else if (auto res_lower = ReadCheatFileFromFolder(title_id, build_id, cheats_dir, false)) { + cheat_entries = std::move(res_lower); + } + + if (cheat_entries) { + for (const auto& cheat : *cheat_entries) { + // Skip master cheat (id 0) with no readable name + if (cheat.cheat_id == 0 && cheat.definition.readable_name[0] == '\0') { + continue; + } + + const std::string cheat_name = cheat.definition.readable_name.data(); + if (cheat_name.empty()) { + continue; + } + + // Create unique key for this cheat: "ModName::CheatName" + const std::string cheat_key = mod->GetName() + "::" + cheat_name; + const auto cheat_disabled = + std::find(disabled.begin(), disabled.end(), cheat_key) != disabled.end(); + + out.push_back({.enabled = !cheat_disabled, + .name = cheat_name, + .version = types, + .type = PatchType::Cheat, + .program_id = title_id, + .title_id = title_id, + .parent_name = mod->GetName()}); + } + } + } } } @@ -592,7 +720,8 @@ std::vector PatchManager::GetPatches(VirtualFile update_raw) const { .version = types, .type = PatchType::Mod, .program_id = title_id, - .title_id = title_id}); + .title_id = title_id, + .parent_name = ""}); } } @@ -624,7 +753,8 @@ std::vector PatchManager::GetPatches(VirtualFile update_raw) const { .version = std::move(list), .type = PatchType::DLC, .program_id = title_id, - .title_id = dlc_match.back().title_id}); + .title_id = dlc_match.back().title_id, + .parent_name = ""}); } return out; diff --git a/src/core/file_sys/patch_manager.h b/src/core/file_sys/patch_manager.h index 552c0fbe23..f2e226a71b 100644 --- a/src/core/file_sys/patch_manager.h +++ b/src/core/file_sys/patch_manager.h @@ -10,7 +10,6 @@ #include "common/common_types.h" #include "core/file_sys/nca_metadata.h" #include "core/file_sys/vfs/vfs_types.h" -#include "core/memory/dmnt_cheat_types.h" namespace Core { class System; @@ -20,13 +19,17 @@ namespace Service::FileSystem { class FileSystemController; } +namespace Service::DMNT { +struct CheatEntry; +} + namespace FileSys { class ContentProvider; class NCA; class NACP; -enum class PatchType { Update, DLC, Mod }; +enum class PatchType { Update, DLC, Mod, Cheat }; struct Patch { bool enabled; @@ -35,6 +38,7 @@ struct Patch { PatchType type; u64 program_id; u64 title_id; + std::string parent_name; }; // A centralized class to manage patches to games. @@ -65,7 +69,7 @@ public: [[nodiscard]] bool HasNSOPatch(const BuildID& build_id, std::string_view name) const; // Creates a CheatList object with all - [[nodiscard]] std::vector CreateCheatList( + [[nodiscard]] std::vector CreateCheatList( const BuildID& build_id) const; // Currently tracked RomFS patches: @@ -76,8 +80,11 @@ public: VirtualFile packed_update_raw = nullptr, bool apply_layeredfs = true) const; - // Returns a vector of patches - [[nodiscard]] std::vector GetPatches(VirtualFile update_raw = nullptr) const; + // Returns a vector of patches including individual cheats + [[nodiscard]] std::vector GetPatches(VirtualFile update_raw = nullptr, + const BuildID& build_id = {}) const; + + [[nodiscard]] BuildID GetBuildID(VirtualFile update_raw = nullptr) const; // If the game update exists, returns the u32 version field in its Meta-type NCA. If that fails, // it will fallback to the Meta-type NCA of the base game. If that fails, the result will be diff --git a/src/core/hle/service/dmnt/cheat_interface.cpp b/src/core/hle/service/dmnt/cheat_interface.cpp new file mode 100644 index 0000000000..2e566814c7 --- /dev/null +++ b/src/core/hle/service/dmnt/cheat_interface.cpp @@ -0,0 +1,238 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "core/hle/service/cmif_serialization.h" +#include "core/hle/service/dmnt/cheat_interface.h" +#include "core/hle/service/dmnt/cheat_process_manager.h" +#include "core/hle/service/dmnt/dmnt_results.h" +#include "core/hle/service/dmnt/dmnt_types.h" + +namespace Service::DMNT { + ICheatInterface::ICheatInterface(Core::System& system_, CheatProcessManager& manager) + : ServiceFramework{system_, "dmnt:cht"}, cheat_process_manager{manager} { + // clang-format off + static const FunctionInfo functions[] = { + {65000, C<&ICheatInterface::HasCheatProcess>, "HasCheatProcess"}, + {65001, C<&ICheatInterface::GetCheatProcessEvent>, "GetCheatProcessEvent"}, + {65002, C<&ICheatInterface::GetCheatProcessMetadata>, "GetCheatProcessMetadata"}, + {65003, C<&ICheatInterface::ForceOpenCheatProcess>, "ForceOpenCheatProcess"}, + {65004, C<&ICheatInterface::PauseCheatProcess>, "PauseCheatProcess"}, + {65005, C<&ICheatInterface::ResumeCheatProcess>, "ResumeCheatProcess"}, + {65006, C<&ICheatInterface::ForceCloseCheatProcess>, "ForceCloseCheatProcess"}, + {65100, C<&ICheatInterface::GetCheatProcessMappingCount>, "GetCheatProcessMappingCount"}, + {65101, C<&ICheatInterface::GetCheatProcessMappings>, "GetCheatProcessMappings"}, + {65102, C<&ICheatInterface::ReadCheatProcessMemory>, "ReadCheatProcessMemory"}, + {65103, C<&ICheatInterface::WriteCheatProcessMemory>, "WriteCheatProcessMemory"}, + {65104, C<&ICheatInterface::QueryCheatProcessMemory>, "QueryCheatProcessMemory"}, + {65200, C<&ICheatInterface::GetCheatCount>, "GetCheatCount"}, + {65201, C<&ICheatInterface::GetCheats>, "GetCheats"}, + {65202, C<&ICheatInterface::GetCheatById>, "GetCheatById"}, + {65203, C<&ICheatInterface::ToggleCheat>, "ToggleCheat"}, + {65204, C<&ICheatInterface::AddCheat>, "AddCheat"}, + {65205, C<&ICheatInterface::RemoveCheat>, "RemoveCheat"}, + {65206, C<&ICheatInterface::ReadStaticRegister>, "ReadStaticRegister"}, + {65207, C<&ICheatInterface::WriteStaticRegister>, "WriteStaticRegister"}, + {65208, C<&ICheatInterface::ResetStaticRegisters>, "ResetStaticRegisters"}, + {65209, C<&ICheatInterface::SetMasterCheat>, "SetMasterCheat"}, + {65300, C<&ICheatInterface::GetFrozenAddressCount>, "GetFrozenAddressCount"}, + {65301, C<&ICheatInterface::GetFrozenAddresses>, "GetFrozenAddresses"}, + {65302, C<&ICheatInterface::GetFrozenAddress>, "GetFrozenAddress"}, + {65303, C<&ICheatInterface::EnableFrozenAddress>, "EnableFrozenAddress"}, + {65304, C<&ICheatInterface::DisableFrozenAddress>, "DisableFrozenAddress"}, + }; + // clang-format on + + RegisterHandlers(functions); + } + + ICheatInterface::~ICheatInterface() = default; + + Result ICheatInterface::HasCheatProcess(Out out_has_cheat) { + LOG_INFO(CheatEngine, "called"); + *out_has_cheat = cheat_process_manager.HasCheatProcess(); + R_SUCCEED(); + } + + Result ICheatInterface::GetCheatProcessEvent(OutCopyHandle out_event) { + LOG_INFO(CheatEngine, "called"); + *out_event = &cheat_process_manager.GetCheatProcessEvent(); + R_SUCCEED(); + } + + Result ICheatInterface::GetCheatProcessMetadata(Out out_metadata) { + LOG_INFO(CheatEngine, "called"); + R_RETURN(cheat_process_manager.GetCheatProcessMetadata(*out_metadata)); + } + + Result ICheatInterface::ForceOpenCheatProcess() { + LOG_INFO(CheatEngine, "called"); + R_UNLESS(R_SUCCEEDED(cheat_process_manager.ForceOpenCheatProcess()), ResultCheatNotAttached); + R_SUCCEED(); + } + + Result ICheatInterface::PauseCheatProcess() { + LOG_INFO(CheatEngine, "called"); + R_RETURN(cheat_process_manager.PauseCheatProcess()); + } + + Result ICheatInterface::ResumeCheatProcess() { + LOG_INFO(CheatEngine, "called"); + R_RETURN(cheat_process_manager.ResumeCheatProcess()); + } + + Result ICheatInterface::ForceCloseCheatProcess() { + LOG_WARNING(CheatEngine, "(STUBBED) called"); + R_RETURN(cheat_process_manager.ForceCloseCheatProcess()); + } + + Result ICheatInterface::GetCheatProcessMappingCount(Out out_count) { + LOG_WARNING(CheatEngine, "(STUBBED) called"); + R_RETURN(cheat_process_manager.GetCheatProcessMappingCount(*out_count)); + } + + Result ICheatInterface::GetCheatProcessMappings( + Out out_count, u64 offset, + OutArray out_mappings) { + LOG_INFO(CheatEngine, "called, offset={}", offset); + R_UNLESS(!out_mappings.empty(), ResultCheatNullBuffer); + R_RETURN(cheat_process_manager.GetCheatProcessMappings(*out_count, offset, out_mappings)); + } + + Result ICheatInterface::ReadCheatProcessMemory(u64 address, u64 size, + OutBuffer out_buffer) { + LOG_DEBUG(CheatEngine, "called, address={}, size={}", address, size); + R_UNLESS(!out_buffer.empty(), ResultCheatNullBuffer); + R_RETURN(cheat_process_manager.ReadCheatProcessMemory(address, size, out_buffer)); + } + + Result ICheatInterface::WriteCheatProcessMemory(u64 address, u64 size, + InBuffer buffer) { + LOG_DEBUG(CheatEngine, "called, address={}, size={}", address, size); + R_UNLESS(!buffer.empty(), ResultCheatNullBuffer); + R_RETURN(cheat_process_manager.WriteCheatProcessMemory(address, size, buffer)); + } + + Result ICheatInterface::QueryCheatProcessMemory(Out out_mapping, + u64 address) { + LOG_WARNING(CheatEngine, "(STUBBED) called, address={}", address); + R_RETURN(cheat_process_manager.QueryCheatProcessMemory(out_mapping, address)); + } + + Result ICheatInterface::GetCheatCount(Out out_count) { + LOG_INFO(CheatEngine, "called"); + R_RETURN(cheat_process_manager.GetCheatCount(*out_count)); + } + + Result ICheatInterface::GetCheats(Out out_count, u64 offset, + OutArray out_cheats) { + LOG_INFO(CheatEngine, "called, offset={}", offset); + R_UNLESS(!out_cheats.empty(), ResultCheatNullBuffer); + R_RETURN(cheat_process_manager.GetCheats(*out_count, offset, out_cheats)); + } + + Result ICheatInterface::GetCheatById(OutLargeData out_cheat, + u32 cheat_id) { + LOG_INFO(CheatEngine, "called, cheat_id={}", cheat_id); + R_RETURN(cheat_process_manager.GetCheatById(out_cheat, cheat_id)); + } + + Result ICheatInterface::ToggleCheat(u32 cheat_id) { + LOG_INFO(CheatEngine, "called, cheat_id={}", cheat_id); + R_RETURN(cheat_process_manager.ToggleCheat(cheat_id)); + } + + Result ICheatInterface::AddCheat( + Out out_cheat_id, bool is_enabled, + InLargeData cheat_definition) { + LOG_INFO(CheatEngine, "called, is_enabled={}", is_enabled); + R_RETURN(cheat_process_manager.AddCheat(*out_cheat_id, is_enabled, *cheat_definition)); + } + + Result ICheatInterface::RemoveCheat(u32 cheat_id) { + LOG_INFO(CheatEngine, "called, cheat_id={}", cheat_id); + R_RETURN(cheat_process_manager.RemoveCheat(cheat_id)); + } + + Result ICheatInterface::ReadStaticRegister(Out out_value, u8 register_index) { + LOG_DEBUG(CheatEngine, "called, register_index={}", register_index); + R_RETURN(cheat_process_manager.ReadStaticRegister(*out_value, register_index)); + } + + Result ICheatInterface::WriteStaticRegister(u8 register_index, u64 value) { + LOG_DEBUG(CheatEngine, "called, register_index={}, value={}", register_index, value); + R_RETURN(cheat_process_manager.WriteStaticRegister(register_index, value)); + } + + Result ICheatInterface::ResetStaticRegisters() { + LOG_INFO(CheatEngine, "called"); + R_RETURN(cheat_process_manager.ResetStaticRegisters()); + } + + Result ICheatInterface::SetMasterCheat( + InLargeData cheat_definition) { + LOG_INFO(CheatEngine, "called, name={}, num_opcodes={}", cheat_definition->readable_name.data(), + cheat_definition->num_opcodes); + R_RETURN(cheat_process_manager.SetMasterCheat(*cheat_definition)); + } + + Result ICheatInterface::GetFrozenAddressCount(Out out_count) { + LOG_INFO(CheatEngine, "called"); + R_RETURN(cheat_process_manager.GetFrozenAddressCount(*out_count)); + } + + Result ICheatInterface::GetFrozenAddresses( + Out out_count, u64 offset, + OutArray out_frozen_address) { + LOG_INFO(CheatEngine, "called, offset={}", offset); + R_UNLESS(!out_frozen_address.empty(), ResultCheatNullBuffer); + R_RETURN(cheat_process_manager.GetFrozenAddresses(*out_count, offset, out_frozen_address)); + } + + Result ICheatInterface::GetFrozenAddress(Out out_frozen_address_entry, + u64 address) { + LOG_INFO(CheatEngine, "called, address={}", address); + R_RETURN(cheat_process_manager.GetFrozenAddress(*out_frozen_address_entry, address)); + } + + Result ICheatInterface::EnableFrozenAddress(Out out_value, u64 address, u64 width) { + LOG_INFO(CheatEngine, "called, address={}, width={}", address, width); + R_UNLESS(width > 0, ResultFrozenAddressInvalidWidth); + R_UNLESS(width <= sizeof(u64), ResultFrozenAddressInvalidWidth); + R_UNLESS((width & (width - 1)) == 0, ResultFrozenAddressInvalidWidth); + R_RETURN(cheat_process_manager.EnableFrozenAddress(*out_value, address, width)); + } + + Result ICheatInterface::DisableFrozenAddress(u64 address) { + LOG_INFO(CheatEngine, "called, address={}", address); + R_RETURN(cheat_process_manager.DisableFrozenAddress(address)); + } + + void ICheatInterface::InitializeCheatManager() { + LOG_INFO(CheatEngine, "called"); + } + + Result ICheatInterface::ReadCheatProcessMemoryUnsafe(u64 process_addr, std::span out_data, + size_t size) { + LOG_DEBUG(CheatEngine, "called, process_addr={}, size={}", process_addr, size); + R_RETURN(cheat_process_manager.ReadCheatProcessMemoryUnsafe(process_addr, &out_data, size)); + } + + Result ICheatInterface::WriteCheatProcessMemoryUnsafe(u64 process_addr, std::span data, + size_t size) { + LOG_DEBUG(CheatEngine, "called, process_addr={}, size={}", process_addr, size); + R_RETURN(cheat_process_manager.WriteCheatProcessMemoryUnsafe(process_addr, &data, size)); + } + + Result ICheatInterface::PauseCheatProcessUnsafe() { + LOG_INFO(CheatEngine, "called"); + R_RETURN(cheat_process_manager.PauseCheatProcessUnsafe()); + } + + Result ICheatInterface::ResumeCheatProcessUnsafe() { + LOG_INFO(CheatEngine, "called"); + R_RETURN(cheat_process_manager.ResumeCheatProcessUnsafe()); + } +} // namespace Service::DMNT diff --git a/src/core/hle/service/dmnt/cheat_interface.h b/src/core/hle/service/dmnt/cheat_interface.h new file mode 100644 index 0000000000..91bf1a7960 --- /dev/null +++ b/src/core/hle/service/dmnt/cheat_interface.h @@ -0,0 +1,88 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "core/hle/service/cmif_types.h" +#include "core/hle/service/kernel_helpers.h" +#include "core/hle/service/service.h" + +namespace Core { + class System; +} + +namespace Kernel { + class KEvent; + class KReadableEvent; +} // namespace Kernel + +namespace Kernel::Svc { + struct MemoryInfo; +} + +namespace Service::DMNT { + struct CheatDefinition; + struct CheatEntry; + struct CheatProcessMetadata; + struct FrozenAddressEntry; + class CheatProcessManager; + + class ICheatInterface final : public ServiceFramework { + public: + explicit ICheatInterface(Core::System& system_, CheatProcessManager& manager); + ~ICheatInterface() override; + + private: + Result HasCheatProcess(Out out_has_cheat); + Result GetCheatProcessEvent(OutCopyHandle out_event); + Result GetCheatProcessMetadata(Out out_metadata); + Result ForceOpenCheatProcess(); + Result PauseCheatProcess(); + Result ResumeCheatProcess(); + Result ForceCloseCheatProcess(); + + Result GetCheatProcessMappingCount(Out out_count); + Result GetCheatProcessMappings( + Out out_count, u64 offset, + OutArray out_mappings); + Result ReadCheatProcessMemory(u64 address, u64 size, + OutBuffer out_buffer); + Result WriteCheatProcessMemory(u64 address, u64 size, InBuffer buffer); + + Result QueryCheatProcessMemory(Out out_mapping, u64 address); + Result GetCheatCount(Out out_count); + Result GetCheats(Out out_count, u64 offset, + OutArray out_cheats); + Result GetCheatById(OutLargeData out_cheat, u32 cheat_id); + Result ToggleCheat(u32 cheat_id); + + Result AddCheat(Out out_cheat_id, bool enabled, + InLargeData cheat_definition); + Result RemoveCheat(u32 cheat_id); + Result ReadStaticRegister(Out out_value, u8 register_index); + Result WriteStaticRegister(u8 register_index, u64 value); + Result ResetStaticRegisters(); + Result SetMasterCheat(InLargeData cheat_definition); + Result GetFrozenAddressCount(Out out_count); + Result GetFrozenAddresses( + Out out_count, u64 offset, + OutArray out_frozen_address); + Result GetFrozenAddress(Out out_frozen_address_entry, u64 address); + Result EnableFrozenAddress(Out out_value, u64 address, u64 width); + Result DisableFrozenAddress(u64 address); + + private: + void InitializeCheatManager(); + + Result ReadCheatProcessMemoryUnsafe(u64 process_addr, std::span out_data, size_t size); + Result WriteCheatProcessMemoryUnsafe(u64 process_addr, std::span data, size_t size); + + Result PauseCheatProcessUnsafe(); + Result ResumeCheatProcessUnsafe(); + + CheatProcessManager& cheat_process_manager; + }; +} // namespace Service::DMNT diff --git a/src/core/hle/service/dmnt/cheat_parser.cpp b/src/core/hle/service/dmnt/cheat_parser.cpp new file mode 100644 index 0000000000..eb8f298fa4 --- /dev/null +++ b/src/core/hle/service/dmnt/cheat_parser.cpp @@ -0,0 +1,120 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include +#include +#include + +#include "core/hle/service/dmnt/cheat_parser.h" + +#include "core/hle/service/dmnt/dmnt_types.h" + +namespace Service::DMNT { + CheatParser::CheatParser() {} + + CheatParser::~CheatParser() = default; + + std::vector CheatParser::Parse(std::string_view data) const { + std::vector out(1); + std::optional current_entry; + + for (std::size_t i = 0; i < data.size(); ++i) { + if (std::isspace(data[i])) { + continue; + } + + if (data[i] == '{') { + current_entry = 0; + + if (out[*current_entry].definition.num_opcodes > 0) { + return {}; + } + + std::size_t name_size{}; + const auto name = ExtractName(name_size, data, i + 1, '}'); + if (name.empty()) { + return {}; + } + + std::memcpy(out[*current_entry].definition.readable_name.data(), name.data(), + std::min(out[*current_entry].definition.readable_name.size(), + name.size())); + out[*current_entry] + .definition.readable_name[out[*current_entry].definition.readable_name.size() - 1] = + '\0'; + + i += name_size + 1; + } else if (data[i] == '[') { + current_entry = out.size(); + out.emplace_back(); + + std::size_t name_size{}; + const auto name = ExtractName(name_size, data, i + 1, ']'); + if (name.empty()) { + return {}; + } + + std::memcpy(out[*current_entry].definition.readable_name.data(), name.data(), + std::min(out[*current_entry].definition.readable_name.size(), + name.size())); + out[*current_entry] + .definition.readable_name[out[*current_entry].definition.readable_name.size() - 1] = + '\0'; + + i += name_size + 1; + } else if (std::isxdigit(data[i])) { + if (!current_entry || out[*current_entry].definition.num_opcodes >= + out[*current_entry].definition.opcodes.size()) { + return {}; + } + + const auto hex = std::string(data.substr(i, 8)); + if (!std::all_of(hex.begin(), hex.end(), ::isxdigit)) { + 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; + + i += 7; // 7 because the for loop will increment by 1 more + } else { + return {}; + } + } + + out[0].enabled = out[0].definition.num_opcodes > 0; + out[0].cheat_id = 0; + + for (u32 i = 1; i < out.size(); ++i) { + out[i].enabled = out[i].definition.num_opcodes > 0; + out[i].cheat_id = i; + } + + return out; + } + + std::string_view CheatParser::ExtractName(std::size_t& out_name_size, std::string_view data, + std::size_t start_index, char match) const { + auto end_index = start_index; + while (data[end_index] != match) { + ++end_index; + if (end_index > data.size()) { + return {}; + } + } + + out_name_size = end_index - start_index; + + // Clamp name if it's too big + if (out_name_size > sizeof(CheatDefinition::readable_name)) { + end_index = start_index + sizeof(CheatDefinition::readable_name); + } + + return data.substr(start_index, end_index - start_index); + } +} // namespace Service::DMNT diff --git a/src/core/hle/service/dmnt/cheat_parser.h b/src/core/hle/service/dmnt/cheat_parser.h new file mode 100644 index 0000000000..9ff248a1e4 --- /dev/null +++ b/src/core/hle/service/dmnt/cheat_parser.h @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include + +namespace Service::DMNT { + struct CheatEntry; + + class CheatParser final { + public: + CheatParser(); + ~CheatParser(); + + std::vector Parse(std::string_view data) const; + + private: + std::string_view ExtractName(std::size_t& out_name_size, std::string_view data, + std::size_t start_index, char match) const; + }; +} // namespace Service::DMNT diff --git a/src/core/hle/service/dmnt/cheat_process_manager.cpp b/src/core/hle/service/dmnt/cheat_process_manager.cpp new file mode 100644 index 0000000000..5fcb5df16f --- /dev/null +++ b/src/core/hle/service/dmnt/cheat_process_manager.cpp @@ -0,0 +1,599 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "core/arm/debug.h" +#include "core/core.h" +#include "core/core_timing.h" +#include "core/hle/service/cmif_serialization.h" +#include "core/hle/service/dmnt/cheat_process_manager.h" +#include "core/hle/service/dmnt/cheat_virtual_machine.h" +#include "core/hle/service/dmnt/dmnt_results.h" +#include "core/hle/service/hid/hid_server.h" +#include "core/hle/service/sm/sm.h" +#include "hid_core/resource_manager.h" +#include "hid_core/resources/npad/npad.h" + +namespace Service::DMNT { + constexpr auto CHEAT_ENGINE_NS = std::chrono::nanoseconds{1000000000 / 12}; + + CheatProcessManager::CheatProcessManager(Core::System& system_) + : system{system_}, service_context{system_, "dmnt:cht"}, core_timing{system_.CoreTiming()} { + update_event = Core::Timing::CreateEvent("CheatEngine::FrameCallback", + [this](s64 time, std::chrono::nanoseconds ns_late) + -> std::optional { + FrameCallback(ns_late); + return std::nullopt; + }); + + for (size_t i = 0; i < MaxCheatCount; i++) { + ResetCheatEntry(i); + } + + cheat_vm = std::make_unique(*this); + + cheat_process_event = service_context.CreateEvent("CheatProcessManager::ProcessEvent"); + unsafe_break_event = service_context.CreateEvent("CheatProcessManager::ProcessEvent"); + } + + CheatProcessManager::~CheatProcessManager() { + service_context.CloseEvent(cheat_process_event); + service_context.CloseEvent(unsafe_break_event); + core_timing.UnscheduleEvent(update_event); + } + + void CheatProcessManager::SetVirtualMachine(std::unique_ptr vm) { + if (vm) { + cheat_vm = std::move(vm); + SetNeedsReloadVm(true); + } + } + + bool CheatProcessManager::HasActiveCheatProcess() { + // Note: This function *MUST* be called only with the cheat lock held. + bool has_cheat_process = + cheat_process_debug_handle != InvalidHandle && + system.ApplicationProcess()->GetProcessId() == cheat_process_metadata.process_id; + + if (!has_cheat_process) { + CloseActiveCheatProcess(); + } + + return has_cheat_process; + } + + void CheatProcessManager::CloseActiveCheatProcess() { + if (cheat_process_debug_handle != InvalidHandle) { + broken_unsafe = false; + unsafe_break_event->Signal(); + core_timing.UnscheduleEvent(update_event); + + // Close resources. + cheat_process_debug_handle = InvalidHandle; + + // Save cheat toggles. + if (always_save_cheat_toggles || should_save_cheat_toggles) { + // TODO: save cheat toggles + should_save_cheat_toggles = false; + } + + cheat_process_metadata = {}; + + ResetAllCheatEntries(); + + { + auto it = frozen_addresses_map.begin(); + while (it != frozen_addresses_map.end()) { + it = frozen_addresses_map.erase(it); + } + } + + cheat_process_event->Signal(); + } + } + + Result CheatProcessManager::EnsureCheatProcess() { + R_UNLESS(HasActiveCheatProcess(), ResultCheatNotAttached); + R_SUCCEED(); + } + + void CheatProcessManager::SetNeedsReloadVm(bool reload) { + needs_reload_vm = reload; + } + + void CheatProcessManager::ResetCheatEntry(size_t i) { + if (i < MaxCheatCount) { + cheat_entries[i] = {}; + cheat_entries[i].cheat_id = static_cast(i); + + SetNeedsReloadVm(true); + } + } + + void CheatProcessManager::ResetAllCheatEntries() { + for (size_t i = 0; i < MaxCheatCount; i++) { + ResetCheatEntry(i); + } + + cheat_vm->ResetStaticRegisters(); + } + + CheatEntry* CheatProcessManager::GetCheatEntryById(size_t i) { + if (i < MaxCheatCount) { + return cheat_entries.data() + i; + } + + return nullptr; + } + + CheatEntry* CheatProcessManager::GetCheatEntryByReadableName(const char* readable_name) { + for (size_t i = 1; i < MaxCheatCount; i++) { + if (std::strncmp(cheat_entries[i].definition.readable_name.data(), readable_name, + sizeof(cheat_entries[i].definition.readable_name)) == 0) { + return cheat_entries.data() + i; + } + } + return nullptr; + } + + CheatEntry* CheatProcessManager::GetFreeCheatEntry() { + // Check all non-master cheats for availability. + for (size_t i = 1; i < MaxCheatCount; i++) { + if (cheat_entries[i].definition.num_opcodes == 0) { + return cheat_entries.data() + i; + } + } + + return nullptr; + } + + bool CheatProcessManager::HasCheatProcess() { + std::scoped_lock lk(cheat_lock); + return HasActiveCheatProcess(); + } + + Kernel::KReadableEvent& CheatProcessManager::GetCheatProcessEvent() const { + return cheat_process_event->GetReadableEvent(); + } + + Result CheatProcessManager::AttachToApplicationProcess(const std::array& build_id, + VAddr main_region_begin, + u64 main_region_size) { + std::scoped_lock lk(cheat_lock); + + { + if (this->HasActiveCheatProcess()) { + this->CloseActiveCheatProcess(); + } + } + + cheat_process_metadata.process_id = system.ApplicationProcess()->GetProcessId(); + + { + const auto& page_table = system.ApplicationProcess()->GetPageTable(); + cheat_process_metadata.program_id = system.GetApplicationProcessProgramID(); + cheat_process_metadata.heap_extents = { + .base = GetInteger(page_table.GetHeapRegionStart()), + .size = page_table.GetHeapRegionSize(), + }; + cheat_process_metadata.aslr_extents = { + .base = GetInteger(page_table.GetAliasCodeRegionStart()), + .size = page_table.GetAliasCodeRegionSize(), + }; + cheat_process_metadata.alias_extents = { + .base = GetInteger(page_table.GetAliasRegionStart()), + .size = page_table.GetAliasRegionSize(), + }; + } + + { + cheat_process_metadata.main_nso_extents = { + .base = main_region_begin, + .size = main_region_size, + }; + cheat_process_metadata.main_nso_build_id = build_id; + } + + cheat_process_debug_handle = cheat_process_metadata.process_id; + + broken_unsafe = false; + unsafe_break_event->Signal(); + + core_timing.ScheduleLoopingEvent(CHEAT_ENGINE_NS, CHEAT_ENGINE_NS, update_event); + LOG_INFO(CheatEngine, "Cheat engine started"); + + // Signal to our fans. + cheat_process_event->Signal(); + + R_SUCCEED(); + } + + Result CheatProcessManager::GetCheatProcessMetadata(CheatProcessMetadata& out_metadata) { + std::scoped_lock lk(cheat_lock); + R_TRY(EnsureCheatProcess()); + + out_metadata = cheat_process_metadata; + R_SUCCEED(); + } + + Result CheatProcessManager::ForceOpenCheatProcess() { + // R_RETURN(AttachToApplicationProcess(false)); + R_SUCCEED(); + } + + Result CheatProcessManager::PauseCheatProcess() { + std::scoped_lock lk(cheat_lock); + + R_TRY(EnsureCheatProcess()); + R_RETURN(PauseCheatProcessUnsafe()); + } + + Result CheatProcessManager::PauseCheatProcessUnsafe() { + broken_unsafe = true; + unsafe_break_event->Clear(); + if (system.ApplicationProcess()->IsSuspended()) { + R_SUCCEED(); + } + R_RETURN(system.ApplicationProcess()->SetActivity(Kernel::Svc::ProcessActivity::Paused)); + } + + Result CheatProcessManager::ResumeCheatProcess() { + std::scoped_lock lk(cheat_lock); + + R_TRY(EnsureCheatProcess()); + R_RETURN(ResumeCheatProcessUnsafe()); + } + + Result CheatProcessManager::ResumeCheatProcessUnsafe() { + broken_unsafe = true; + unsafe_break_event->Clear(); + if (!system.ApplicationProcess()->IsSuspended()) { + R_SUCCEED(); + } + system.ApplicationProcess()->SetActivity(Kernel::Svc::ProcessActivity::Runnable); + R_SUCCEED(); + } + + Result CheatProcessManager::ForceCloseCheatProcess() { + CloseActiveCheatProcess(); + R_SUCCEED(); + } + + Result CheatProcessManager::GetCheatProcessMappingCount(u64& out_count) { + std::scoped_lock lk(cheat_lock); + R_TRY(this->EnsureCheatProcess()); + + // TODO: Call svc::QueryDebugProcessMemory + + out_count = 0; + R_SUCCEED(); + } + + Result CheatProcessManager::GetCheatProcessMappings( + u64& out_count, u64 offset, std::span out_mappings) { + std::scoped_lock lk(cheat_lock); + R_TRY(this->EnsureCheatProcess()); + + // TODO: Call svc::QueryDebugProcessMemory + + out_count = 0; + R_SUCCEED(); + } + + Result CheatProcessManager::ReadCheatProcessMemory(u64 process_address, u64 size, + std::span out_data) { + std::scoped_lock lk(cheat_lock); + + R_TRY(EnsureCheatProcess()); + R_RETURN(ReadCheatProcessMemoryUnsafe(process_address, &out_data, size)); + } + + Result CheatProcessManager::ReadCheatProcessMemoryUnsafe(u64 process_address, void* out_data, + size_t size) { + if (!system.ApplicationMemory().IsValidVirtualAddress(process_address)) { + std::memset(out_data, 0, size); + R_SUCCEED(); + } + + system.ApplicationMemory().ReadBlock(process_address, out_data, size); + R_SUCCEED(); + } + + Result CheatProcessManager::WriteCheatProcessMemory(u64 process_address, u64 size, + std::span data) { + std::scoped_lock lk(cheat_lock); + + R_TRY(EnsureCheatProcess()); + R_RETURN(WriteCheatProcessMemoryUnsafe(process_address, &data, size)); + } + + Result CheatProcessManager::WriteCheatProcessMemoryUnsafe(u64 process_address, const void* data, + size_t size) { + if (!system.ApplicationMemory().IsValidVirtualAddress(process_address)) { + R_SUCCEED(); + } + + if (system.ApplicationMemory().WriteBlock(process_address, data, size)) { + Core::InvalidateInstructionCacheRange(system.ApplicationProcess(), process_address, size); + } + + R_SUCCEED(); + } + + Result CheatProcessManager::QueryCheatProcessMemory(Out mapping, + u64 address) { + std::scoped_lock lk(cheat_lock); + R_TRY(this->EnsureCheatProcess()); + + // TODO: Call svc::QueryDebugProcessMemory + R_SUCCEED(); + } + + Result CheatProcessManager::GetCheatCount(u64& out_count) { + std::scoped_lock lk(cheat_lock); + R_TRY(EnsureCheatProcess()); + + out_count = std::count_if(cheat_entries.begin(), cheat_entries.end(), + [](const auto& entry) { return entry.definition.num_opcodes != 0; }); + R_SUCCEED(); + } + + Result CheatProcessManager::GetCheats(u64& out_count, u64 offset, + std::span out_cheats) { + std::scoped_lock lk(cheat_lock); + R_TRY(EnsureCheatProcess()); + + size_t count = 0, total_count = 0; + for (size_t i = 0; i < MaxCheatCount && count < out_cheats.size(); i++) { + if (cheat_entries[i].definition.num_opcodes) { + total_count++; + if (total_count > offset) { + out_cheats[count++] = cheat_entries[i]; + } + } + } + + out_count = count; + R_SUCCEED(); + } + + Result CheatProcessManager::GetCheatById(CheatEntry* out_cheat, u32 cheat_id) { + std::scoped_lock lk(cheat_lock); + R_TRY(EnsureCheatProcess()); + + const CheatEntry* entry = GetCheatEntryById(cheat_id); + R_UNLESS(entry != nullptr, ResultCheatUnknownId); + R_UNLESS(entry->definition.num_opcodes != 0, ResultCheatUnknownId); + + *out_cheat = *entry; + R_SUCCEED(); + } + + Result CheatProcessManager::ToggleCheat(u32 cheat_id) { + std::scoped_lock lk(cheat_lock); + R_TRY(EnsureCheatProcess()); + + CheatEntry* entry = GetCheatEntryById(cheat_id); + R_UNLESS(entry != nullptr, ResultCheatUnknownId); + R_UNLESS(entry->definition.num_opcodes != 0, ResultCheatUnknownId); + + R_UNLESS(cheat_id != 0, ResultCheatCannotDisable); + + entry->enabled = !entry->enabled; + + SetNeedsReloadVm(true); + + R_SUCCEED(); + } + + Result CheatProcessManager::AddCheat(u32& out_cheat_id, bool enabled, + const CheatDefinition& cheat_definition) { + std::scoped_lock lk(cheat_lock); + R_TRY(EnsureCheatProcess()); + + R_UNLESS(cheat_definition.num_opcodes != 0, ResultCheatInvalid); + R_UNLESS(cheat_definition.num_opcodes <= cheat_definition.opcodes.size(), ResultCheatInvalid); + + CheatEntry* new_entry = GetFreeCheatEntry(); + R_UNLESS(new_entry != nullptr, ResultCheatOutOfResource); + + new_entry->enabled = enabled; + new_entry->definition = cheat_definition; + + SetNeedsReloadVm(true); + + out_cheat_id = new_entry->cheat_id; + + R_SUCCEED(); + } + + Result CheatProcessManager::RemoveCheat(u32 cheat_id) { + std::scoped_lock lk(cheat_lock); + R_TRY(EnsureCheatProcess()); + R_UNLESS(cheat_id < MaxCheatCount, ResultCheatUnknownId); + + ResetCheatEntry(cheat_id); + SetNeedsReloadVm(true); + R_SUCCEED(); + } + + Result CheatProcessManager::ReadStaticRegister(u64& out_value, u64 register_index) { + std::scoped_lock lk(cheat_lock); + R_TRY(EnsureCheatProcess()); + R_UNLESS(register_index < CheatVirtualMachine::NumStaticRegisters, ResultCheatInvalid); + + out_value = cheat_vm->GetStaticRegister(register_index); + R_SUCCEED(); + } + + Result CheatProcessManager::WriteStaticRegister(u64 register_index, u64 value) { + std::scoped_lock lk(cheat_lock); + R_TRY(EnsureCheatProcess()); + R_UNLESS(register_index < CheatVirtualMachine::NumStaticRegisters, ResultCheatInvalid); + + cheat_vm->SetStaticRegister(register_index, value); + R_SUCCEED(); + } + + Result CheatProcessManager::ResetStaticRegisters() { + std::scoped_lock lk(cheat_lock); + R_TRY(EnsureCheatProcess()); + + cheat_vm->ResetStaticRegisters(); + R_SUCCEED(); + } + + Result CheatProcessManager::SetMasterCheat(const CheatDefinition& cheat_definition) { + std::scoped_lock lk(cheat_lock); + R_TRY(EnsureCheatProcess()); + + R_UNLESS(cheat_definition.num_opcodes != 0, ResultCheatInvalid); + R_UNLESS(cheat_definition.num_opcodes <= cheat_definition.opcodes.size(), ResultCheatInvalid); + + cheat_entries[0] = { + .enabled = true, + .definition = cheat_definition, + }; + + SetNeedsReloadVm(true); + + R_SUCCEED(); + } + + Result CheatProcessManager::GetFrozenAddressCount(u64& out_count) { + std::scoped_lock lk(cheat_lock); + R_TRY(EnsureCheatProcess()); + + out_count = std::distance(frozen_addresses_map.begin(), frozen_addresses_map.end()); + R_SUCCEED(); + } + + Result CheatProcessManager::GetFrozenAddresses(u64& out_count, u64 offset, + std::span out_frozen_address) { + std::scoped_lock lk(cheat_lock); + R_TRY(EnsureCheatProcess()); + + u64 total_count = 0, written_count = 0; + for (const auto& [address, value] : frozen_addresses_map) { + if (written_count >= out_frozen_address.size()) { + break; + } + + if (offset <= total_count) { + out_frozen_address[written_count].address = address; + out_frozen_address[written_count].value = value; + written_count++; + } + total_count++; + } + + out_count = written_count; + R_SUCCEED(); + } + + Result CheatProcessManager::GetFrozenAddress(FrozenAddressEntry& out_frozen_address_entry, + u64 address) { + std::scoped_lock lk(cheat_lock); + R_TRY(EnsureCheatProcess()); + + const auto it = frozen_addresses_map.find(address); + R_UNLESS(it != frozen_addresses_map.end(), ResultFrozenAddressNotFound); + + out_frozen_address_entry = { + .address = it->first, + .value = it->second, + }; + R_SUCCEED(); + } + + Result CheatProcessManager::EnableFrozenAddress(u64& out_value, u64 address, u64 width) { + std::scoped_lock lk(cheat_lock); + R_TRY(EnsureCheatProcess()); + + const auto it = frozen_addresses_map.find(address); + R_UNLESS(it == frozen_addresses_map.end(), ResultFrozenAddressAlreadyExists); + + FrozenAddressValue value{}; + value.width = static_cast(width); + R_TRY(ReadCheatProcessMemoryUnsafe(address, &value.value, width)); + + frozen_addresses_map.insert({address, value}); + out_value = value.value; + R_SUCCEED(); + } + + Result CheatProcessManager::DisableFrozenAddress(u64 address) { + std::scoped_lock lk(cheat_lock); + R_TRY(EnsureCheatProcess()); + + const auto it = frozen_addresses_map.find(address); + R_UNLESS(it != frozen_addresses_map.end(), ResultFrozenAddressNotFound); + + frozen_addresses_map.erase(it); + + R_SUCCEED(); + } + + u64 CheatProcessManager::HidKeysDown() const { + const auto hid = system.ServiceManager().GetService("hid"); + if (hid == nullptr) { + LOG_WARNING(CheatEngine, "Attempted to read input state, but hid is not initialized!"); + return 0; + } + + const auto applet_resource = hid->GetResourceManager(); + if (applet_resource == nullptr || applet_resource->GetNpad() == nullptr) { + LOG_WARNING(CheatEngine, + "Attempted to read input state, but applet resource is not initialized!"); + return 0; + } + + const auto press_state = applet_resource->GetNpad()->GetAndResetPressState(); + return static_cast(press_state & Core::HID::NpadButton::All); + } + + void CheatProcessManager::DebugLog(u8 id, u64 value) const { + LOG_INFO(CheatEngine, "Cheat triggered DebugLog: ID '{:01X}' Value '{:016X}'", id, value); + } + + void CheatProcessManager::CommandLog(std::string_view data) const { + LOG_DEBUG(CheatEngine, "[DmntCheatVm]: {}", + data.back() == '\n' ? data.substr(0, data.size() - 1) : data); + } + + void CheatProcessManager::FrameCallback(std::chrono::nanoseconds ns_late) { + std::scoped_lock lk(cheat_lock); + + if (cheat_vm == nullptr) { + LOG_DEBUG(CheatEngine, "FrameCallback: VM is null"); + return; + } + + if (needs_reload_vm) { + LOG_INFO(CheatEngine, "Reloading cheat VM with {} entries", cheat_entries.size()); + + size_t enabled_count = 0; + for (const auto& entry : cheat_entries) { + if (entry.enabled && entry.definition.num_opcodes > 0) { + enabled_count++; + LOG_INFO(CheatEngine, " Cheat '{}': {} opcodes, enabled={}", + entry.definition.readable_name.data(), + entry.definition.num_opcodes, entry.enabled); + } + } + LOG_INFO(CheatEngine, "Total enabled cheats: {}", enabled_count); + + cheat_vm->LoadProgram(cheat_entries); + LOG_INFO(CheatEngine, "Cheat VM loaded, program size: {}", cheat_vm->GetProgramSize()); + needs_reload_vm = false; + } + + if (cheat_vm->GetProgramSize() == 0) { + return; + } + + cheat_vm->Execute(cheat_process_metadata); + } +} // namespace Service::DMNT diff --git a/src/core/hle/service/dmnt/cheat_process_manager.h b/src/core/hle/service/dmnt/cheat_process_manager.h new file mode 100644 index 0000000000..bbf0782921 --- /dev/null +++ b/src/core/hle/service/dmnt/cheat_process_manager.h @@ -0,0 +1,126 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include + +#include "core/hle/service/cmif_types.h" +#include "core/hle/service/dmnt/dmnt_types.h" +#include "core/hle/service/kernel_helpers.h" +#include "core/hle/service/service.h" + +#include "common/intrusive_red_black_tree.h" + +namespace Core { + class System; +} + +namespace Kernel { + class KEvent; + class KReadableEvent; +} // namespace Kernel + +namespace Kernel::Svc { + struct MemoryInfo; +} + +namespace Service::DMNT { + class CheatVirtualMachine; + + class CheatProcessManager final { + public: + static constexpr size_t MaxCheatCount = 0x80; + static constexpr size_t MaxFrozenAddressCount = 0x80; + + CheatProcessManager(Core::System& system_); + ~CheatProcessManager(); + + void SetVirtualMachine(std::unique_ptr vm); + + bool HasCheatProcess(); + Kernel::KReadableEvent& GetCheatProcessEvent() const; + Result GetCheatProcessMetadata(CheatProcessMetadata& out_metadata); + Result AttachToApplicationProcess(const std::array& build_id, VAddr main_region_begin, + u64 main_region_size); + Result ForceOpenCheatProcess(); + Result PauseCheatProcess(); + Result PauseCheatProcessUnsafe(); + Result ResumeCheatProcess(); + Result ResumeCheatProcessUnsafe(); + Result ForceCloseCheatProcess(); + + Result GetCheatProcessMappingCount(u64& out_count); + Result GetCheatProcessMappings(u64& out_count, u64 offset, + std::span out_mappings); + Result ReadCheatProcessMemory(u64 process_address, u64 size, std::span out_data); + Result ReadCheatProcessMemoryUnsafe(u64 process_address, void* out_data, size_t size); + Result WriteCheatProcessMemory(u64 process_address, u64 size, std::span data); + Result WriteCheatProcessMemoryUnsafe(u64 process_address, const void* data, size_t size); + + Result QueryCheatProcessMemory(Out mapping, u64 address); + Result GetCheatCount(u64& out_count); + Result GetCheats(u64& out_count, u64 offset, std::span out_cheats); + Result GetCheatById(CheatEntry* out_cheat, u32 cheat_id); + Result ToggleCheat(u32 cheat_id); + + Result AddCheat(u32& out_cheat_id, bool enabled, const CheatDefinition& cheat_definition); + Result RemoveCheat(u32 cheat_id); + Result ReadStaticRegister(u64& out_value, u64 register_index); + Result WriteStaticRegister(u64 register_index, u64 value); + Result ResetStaticRegisters(); + Result SetMasterCheat(const CheatDefinition& cheat_definition); + Result GetFrozenAddressCount(u64& out_count); + Result GetFrozenAddresses(u64& out_count, u64 offset, + std::span out_frozen_address); + Result GetFrozenAddress(FrozenAddressEntry& out_frozen_address_entry, u64 address); + Result EnableFrozenAddress(u64& out_value, u64 address, u64 width); + Result DisableFrozenAddress(u64 address); + + u64 HidKeysDown() const; + void DebugLog(u8 id, u64 value) const; + void CommandLog(std::string_view data) const; + + private: + bool HasActiveCheatProcess(); + void CloseActiveCheatProcess(); + Result EnsureCheatProcess(); + void SetNeedsReloadVm(bool reload); + void ResetCheatEntry(size_t i); + void ResetAllCheatEntries(); + CheatEntry* GetCheatEntryById(size_t i); + CheatEntry* GetCheatEntryByReadableName(const char* readable_name); + CheatEntry* GetFreeCheatEntry(); + + void FrameCallback(std::chrono::nanoseconds ns_late); + + static constexpr u64 InvalidHandle = 0; + + mutable std::mutex cheat_lock; + Kernel::KEvent* unsafe_break_event; + + Kernel::KEvent* cheat_process_event; + u64 cheat_process_debug_handle = InvalidHandle; + CheatProcessMetadata cheat_process_metadata = {}; + + bool broken_unsafe = false; + bool needs_reload_vm = false; + std::unique_ptr cheat_vm; + + bool enable_cheats_by_default = true; + bool always_save_cheat_toggles = false; + bool should_save_cheat_toggles = false; + std::array cheat_entries = {}; + // TODO: Replace with IntrusiveRedBlackTree + std::map frozen_addresses_map = {}; + + Core::System& system; + KernelHelpers::ServiceContext service_context; + std::shared_ptr update_event; + Core::Timing::CoreTiming& core_timing; + }; +} // namespace Service::DMNT diff --git a/src/core/hle/service/dmnt/cheat_virtual_machine.cpp b/src/core/hle/service/dmnt/cheat_virtual_machine.cpp new file mode 100644 index 0000000000..6d78f1115f --- /dev/null +++ b/src/core/hle/service/dmnt/cheat_virtual_machine.cpp @@ -0,0 +1,1269 @@ +// SPDX-FileCopyrightText: Copyright 2025 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 + +#include + +#include "common/assert.h" +#include "common/scope_exit.h" +#include "core/hle/service/dmnt/cheat_process_manager.h" +#include "core/hle/service/dmnt/cheat_virtual_machine.h" + +namespace Service::DMNT { + CheatVirtualMachine::CheatVirtualMachine(CheatProcessManager& cheat_manager) + : manager(cheat_manager) {} + + CheatVirtualMachine::~CheatVirtualMachine() = default; + + void CheatVirtualMachine::DebugLog(u32 log_id, u64 value) const { + manager.DebugLog(static_cast(log_id), value); + } + + void CheatVirtualMachine::LogOpcode(const CheatVmOpcode& opcode) const { + if (auto store_static = std::get_if(&opcode.opcode)) { + manager.CommandLog("Opcode: Store Static"); + manager.CommandLog(fmt::format("Bit Width: {:X}", store_static->bit_width)); + manager.CommandLog( + fmt::format("Mem Type: {:X}", static_cast(store_static->mem_type))); + manager.CommandLog(fmt::format("Reg Idx: {:X}", store_static->offset_register)); + manager.CommandLog(fmt::format("Rel Addr: {:X}", store_static->rel_address)); + manager.CommandLog(fmt::format("Value: {:X}", store_static->value.bit64)); + } else if (auto begin_cond = std::get_if(&opcode.opcode)) { + manager.CommandLog("Opcode: Begin Conditional"); + manager.CommandLog(fmt::format("Bit Width: {:X}", begin_cond->bit_width)); + manager.CommandLog(fmt::format("Mem Type: {:X}", static_cast(begin_cond->mem_type))); + manager.CommandLog(fmt::format("Cond Type: {:X}", static_cast(begin_cond->cond_type))); + manager.CommandLog(fmt::format("Rel Addr: {:X}", begin_cond->rel_address)); + manager.CommandLog(fmt::format("Value: {:X}", begin_cond->value.bit64)); + } else if (std::holds_alternative(opcode.opcode)) { + manager.CommandLog("Opcode: End Conditional"); + } else if (auto ctrl_loop = std::get_if(&opcode.opcode)) { + if (ctrl_loop->start_loop) { + manager.CommandLog("Opcode: Start Loop"); + manager.CommandLog(fmt::format("Reg Idx: {:X}", ctrl_loop->reg_index)); + manager.CommandLog(fmt::format("Num Iters: {:X}", ctrl_loop->num_iters)); + } else { + manager.CommandLog("Opcode: End Loop"); + manager.CommandLog(fmt::format("Reg Idx: {:X}", ctrl_loop->reg_index)); + } + } else if (auto ldr_static = std::get_if(&opcode.opcode)) { + manager.CommandLog("Opcode: Load Register Static"); + manager.CommandLog(fmt::format("Reg Idx: {:X}", ldr_static->reg_index)); + manager.CommandLog(fmt::format("Value: {:X}", ldr_static->value)); + } else if (auto ldr_memory = std::get_if(&opcode.opcode)) { + manager.CommandLog("Opcode: Load Register Memory"); + manager.CommandLog(fmt::format("Bit Width: {:X}", ldr_memory->bit_width)); + manager.CommandLog(fmt::format("Reg Idx: {:X}", ldr_memory->reg_index)); + manager.CommandLog(fmt::format("Mem Type: {:X}", static_cast(ldr_memory->mem_type))); + manager.CommandLog(fmt::format("From Reg: {:d}", ldr_memory->load_from_reg)); + manager.CommandLog(fmt::format("Rel Addr: {:X}", ldr_memory->rel_address)); + } else if (auto str_static = std::get_if(&opcode.opcode)) { + manager.CommandLog("Opcode: Store Static to Address"); + manager.CommandLog(fmt::format("Bit Width: {:X}", str_static->bit_width)); + manager.CommandLog(fmt::format("Reg Idx: {:X}", str_static->reg_index)); + if (str_static->add_offset_reg) { + manager.CommandLog(fmt::format("O Reg Idx: {:X}", str_static->offset_reg_index)); + } + manager.CommandLog(fmt::format("Incr Reg: {:d}", str_static->increment_reg)); + manager.CommandLog(fmt::format("Value: {:X}", str_static->value)); + } else if (auto perform_math_static = + std::get_if(&opcode.opcode)) { + manager.CommandLog("Opcode: Perform Static Arithmetic"); + manager.CommandLog(fmt::format("Bit Width: {:X}", perform_math_static->bit_width)); + manager.CommandLog(fmt::format("Reg Idx: {:X}", perform_math_static->reg_index)); + manager.CommandLog( + fmt::format("Math Type: {:X}", static_cast(perform_math_static->math_type))); + manager.CommandLog(fmt::format("Value: {:X}", perform_math_static->value)); + } else if (auto begin_keypress_cond = + std::get_if(&opcode.opcode)) { + manager.CommandLog("Opcode: Begin Keypress Conditional"); + manager.CommandLog(fmt::format("Key Mask: {:X}", begin_keypress_cond->key_mask)); + } else if (auto perform_math_reg = + std::get_if(&opcode.opcode)) { + manager.CommandLog("Opcode: Perform Register Arithmetic"); + manager.CommandLog(fmt::format("Bit Width: {:X}", perform_math_reg->bit_width)); + manager.CommandLog(fmt::format("Dst Idx: {:X}", perform_math_reg->dst_reg_index)); + manager.CommandLog(fmt::format("Src1 Idx: {:X}", perform_math_reg->src_reg_1_index)); + if (perform_math_reg->has_immediate) { + manager.CommandLog(fmt::format("Value: {:X}", perform_math_reg->value.bit64)); + } else { + manager.CommandLog(fmt::format("Src2 Idx: {:X}", perform_math_reg->src_reg_2_index)); + } + } else if (auto str_register = std::get_if(&opcode.opcode)) { + manager.CommandLog("Opcode: Store Register to Address"); + manager.CommandLog(fmt::format("Bit Width: {:X}", str_register->bit_width)); + manager.CommandLog(fmt::format("S Reg Idx: {:X}", str_register->str_reg_index)); + manager.CommandLog(fmt::format("A Reg Idx: {:X}", str_register->addr_reg_index)); + manager.CommandLog(fmt::format("Incr Reg: {:d}", str_register->increment_reg)); + switch (str_register->ofs_type) { + case StoreRegisterOffsetType::None: + break; + case StoreRegisterOffsetType::Reg: + manager.CommandLog(fmt::format("O Reg Idx: {:X}", str_register->ofs_reg_index)); + break; + case StoreRegisterOffsetType::Imm: + manager.CommandLog(fmt::format("Rel Addr: {:X}", str_register->rel_address)); + break; + case StoreRegisterOffsetType::MemReg: + manager.CommandLog( + fmt::format("Mem Type: {:X}", static_cast(str_register->mem_type))); + break; + case StoreRegisterOffsetType::MemImm: + case StoreRegisterOffsetType::MemImmReg: + manager.CommandLog( + fmt::format("Mem Type: {:X}", static_cast(str_register->mem_type))); + manager.CommandLog(fmt::format("Rel Addr: {:X}", str_register->rel_address)); + break; + } + } else if (auto begin_reg_cond = std::get_if(&opcode.opcode)) { + manager.CommandLog("Opcode: Begin Register Conditional"); + manager.CommandLog(fmt::format("Bit Width: {:X}", begin_reg_cond->bit_width)); + manager.CommandLog( + fmt::format("Cond Type: {:X}", static_cast(begin_reg_cond->cond_type))); + manager.CommandLog(fmt::format("V Reg Idx: {:X}", begin_reg_cond->val_reg_index)); + switch (begin_reg_cond->comp_type) { + case CompareRegisterValueType::StaticValue: + manager.CommandLog("Comp Type: Static Value"); + manager.CommandLog(fmt::format("Value: {:X}", begin_reg_cond->value.bit64)); + break; + case CompareRegisterValueType::OtherRegister: + manager.CommandLog("Comp Type: Other Register"); + manager.CommandLog(fmt::format("X Reg Idx: {:X}", begin_reg_cond->other_reg_index)); + break; + case CompareRegisterValueType::MemoryRelAddr: + manager.CommandLog("Comp Type: Memory Relative Address"); + manager.CommandLog( + fmt::format("Mem Type: {:X}", static_cast(begin_reg_cond->mem_type))); + manager.CommandLog(fmt::format("Rel Addr: {:X}", begin_reg_cond->rel_address)); + break; + case CompareRegisterValueType::MemoryOfsReg: + manager.CommandLog("Comp Type: Memory Offset Register"); + manager.CommandLog( + fmt::format("Mem Type: {:X}", static_cast(begin_reg_cond->mem_type))); + manager.CommandLog(fmt::format("O Reg Idx: {:X}", begin_reg_cond->ofs_reg_index)); + break; + case CompareRegisterValueType::RegisterRelAddr: + manager.CommandLog("Comp Type: Register Relative Address"); + manager.CommandLog(fmt::format("A Reg Idx: {:X}", begin_reg_cond->addr_reg_index)); + manager.CommandLog(fmt::format("Rel Addr: {:X}", begin_reg_cond->rel_address)); + break; + case CompareRegisterValueType::RegisterOfsReg: + manager.CommandLog("Comp Type: Register Offset Register"); + manager.CommandLog(fmt::format("A Reg Idx: {:X}", begin_reg_cond->addr_reg_index)); + manager.CommandLog(fmt::format("O Reg Idx: {:X}", begin_reg_cond->ofs_reg_index)); + break; + } + } else if (auto save_restore_reg = std::get_if(&opcode.opcode)) { + manager.CommandLog("Opcode: Save or Restore Register"); + manager.CommandLog(fmt::format("Dst Idx: {:X}", save_restore_reg->dst_index)); + manager.CommandLog(fmt::format("Src Idx: {:X}", save_restore_reg->src_index)); + manager.CommandLog( + fmt::format("Op Type: {:d}", static_cast(save_restore_reg->op_type))); + } else if (auto save_restore_regmask = + std::get_if(&opcode.opcode)) { + manager.CommandLog("Opcode: Save or Restore Register Mask"); + manager.CommandLog( + fmt::format("Op Type: {:d}", static_cast(save_restore_regmask->op_type))); + for (std::size_t i = 0; i < NumRegisters; i++) { + manager.CommandLog( + fmt::format("Act[{:02X}]: {:d}", i, save_restore_regmask->should_operate[i])); + } + } else if (auto rw_static_reg = std::get_if(&opcode.opcode)) { + manager.CommandLog("Opcode: Read/Write Static Register"); + if (rw_static_reg->static_idx < NumReadableStaticRegisters) { + manager.CommandLog("Op Type: ReadStaticRegister"); + } else { + manager.CommandLog("Op Type: WriteStaticRegister"); + } + manager.CommandLog(fmt::format("Reg Idx {:X}", rw_static_reg->idx)); + manager.CommandLog(fmt::format("Stc Idx {:X}", rw_static_reg->static_idx)); + } else if (auto debug_log = std::get_if(&opcode.opcode)) { + manager.CommandLog("Opcode: Debug Log"); + manager.CommandLog(fmt::format("Bit Width: {:X}", debug_log->bit_width)); + manager.CommandLog(fmt::format("Log ID: {:X}", debug_log->log_id)); + manager.CommandLog(fmt::format("Val Type: {:X}", static_cast(debug_log->val_type))); + switch (debug_log->val_type) { + case DebugLogValueType::RegisterValue: + manager.CommandLog("Val Type: Register Value"); + manager.CommandLog(fmt::format("X Reg Idx: {:X}", debug_log->val_reg_index)); + break; + case DebugLogValueType::MemoryRelAddr: + manager.CommandLog("Val Type: Memory Relative Address"); + manager.CommandLog( + fmt::format("Mem Type: {:X}", static_cast(debug_log->mem_type))); + manager.CommandLog(fmt::format("Rel Addr: {:X}", debug_log->rel_address)); + break; + case DebugLogValueType::MemoryOfsReg: + manager.CommandLog("Val Type: Memory Offset Register"); + manager.CommandLog( + fmt::format("Mem Type: {:X}", static_cast(debug_log->mem_type))); + manager.CommandLog(fmt::format("O Reg Idx: {:X}", debug_log->ofs_reg_index)); + break; + case DebugLogValueType::RegisterRelAddr: + manager.CommandLog("Val Type: Register Relative Address"); + manager.CommandLog(fmt::format("A Reg Idx: {:X}", debug_log->addr_reg_index)); + manager.CommandLog(fmt::format("Rel Addr: {:X}", debug_log->rel_address)); + break; + case DebugLogValueType::RegisterOfsReg: + manager.CommandLog("Val Type: Register Offset Register"); + manager.CommandLog(fmt::format("A Reg Idx: {:X}", debug_log->addr_reg_index)); + manager.CommandLog(fmt::format("O Reg Idx: {:X}", debug_log->ofs_reg_index)); + break; + } + } else if (auto instr = std::get_if(&opcode.opcode)) { + manager.CommandLog(fmt::format("Unknown opcode: {:X}", static_cast(instr->opcode))); + } + } + + bool CheatVirtualMachine::DecodeNextOpcode(CheatVmOpcode& out) { + // If we've ever seen a decode failure, return false. + bool valid = decode_success; + CheatVmOpcode opcode = {}; + SCOPE_EXIT { + decode_success &= valid; + if (valid) { + out = opcode; + } + }; + + // Helper function for getting instruction dwords. + const auto GetNextDword = [&] { + if (instruction_ptr >= num_opcodes) { + valid = false; + return static_cast(0); + } + return program[instruction_ptr++]; + }; + + // Helper function for parsing a VmInt. + const auto GetNextVmInt = [&](const u32 bit_width) { + VmInt val{}; + + const u32 first_dword = GetNextDword(); + switch (bit_width) { + case 1: + val.bit8 = static_cast(first_dword); + break; + case 2: + val.bit16 = static_cast(first_dword); + break; + case 4: + val.bit32 = first_dword; + break; + case 8: + val.bit64 = (static_cast(first_dword) << 32ul) | static_cast(GetNextDword()); + break; + } + + return val; + }; + + // Read opcode. + const u32 first_dword = GetNextDword(); + if (!valid) { + return valid; + } + + auto opcode_type = static_cast(((first_dword >> 28) & 0xF)); + if (opcode_type >= CheatVmOpcodeType::ExtendedWidth) { + opcode_type = static_cast((static_cast(opcode_type) << 4) | + ((first_dword >> 24) & 0xF)); + } + if (opcode_type >= CheatVmOpcodeType::DoubleExtendedWidth) { + opcode_type = static_cast((static_cast(opcode_type) << 4) | + ((first_dword >> 20) & 0xF)); + } + + // detect condition start. + switch (opcode_type) { + case CheatVmOpcodeType::BeginConditionalBlock: + case CheatVmOpcodeType::BeginKeypressConditionalBlock: + case CheatVmOpcodeType::BeginRegisterConditionalBlock: + opcode.begin_conditional_block = true; + break; + default: + opcode.begin_conditional_block = false; + break; + } + + switch (opcode_type) { + case CheatVmOpcodeType::StoreStatic: { + // 0TMR00AA AAAAAAAA YYYYYYYY (YYYYYYYY) + // Read additional words. + const u32 second_dword = GetNextDword(); + const u32 bit_width = (first_dword >> 24) & 0xF; + + opcode.opcode = StoreStaticOpcode{ + .bit_width = bit_width, + .mem_type = static_cast((first_dword >> 20) & 0xF), + .offset_register = (first_dword >> 16) & 0xF, + .rel_address = (static_cast(first_dword & 0xFF) << 32) | second_dword, + .value = GetNextVmInt(bit_width), + }; + } break; + case CheatVmOpcodeType::BeginConditionalBlock: { + // 1TMC00AA AAAAAAAA YYYYYYYY (YYYYYYYY) + // Read additional words. + const u32 second_dword = GetNextDword(); + const u32 bit_width = (first_dword >> 24) & 0xF; + + opcode.opcode = BeginConditionalOpcode{ + .bit_width = bit_width, + .mem_type = static_cast((first_dword >> 20) & 0xF), + .cond_type = static_cast((first_dword >> 16) & 0xF), + .rel_address = (static_cast(first_dword & 0xFF) << 32) | second_dword, + .value = GetNextVmInt(bit_width), + }; + } break; + case CheatVmOpcodeType::EndConditionalBlock: { + // 20000000 + opcode.opcode = EndConditionalOpcode{ + .is_else = ((first_dword >> 24) & 0xf) == 1, + }; + } break; + case CheatVmOpcodeType::ControlLoop: { + // 300R0000 VVVVVVVV + // 310R0000 + // Parse register, whether loop start or loop end. + ControlLoopOpcode ctrl_loop{ + .start_loop = ((first_dword >> 24) & 0xF) == 0, + .reg_index = (first_dword >> 20) & 0xF, + .num_iters = 0, + }; + + // Read number of iters if loop start. + if (ctrl_loop.start_loop) { + ctrl_loop.num_iters = GetNextDword(); + } + opcode.opcode = ctrl_loop; + } break; + case CheatVmOpcodeType::LoadRegisterStatic: { + // 400R0000 VVVVVVVV VVVVVVVV + // Read additional words. + opcode.opcode = LoadRegisterStaticOpcode{ + .reg_index = (first_dword >> 16) & 0xF, + .value = (static_cast(GetNextDword()) << 32) | GetNextDword(), + }; + } break; + case CheatVmOpcodeType::LoadRegisterMemory: { + // 5TMRI0AA AAAAAAAA + // Read additional words. + const u32 second_dword = GetNextDword(); + opcode.opcode = LoadRegisterMemoryOpcode{ + .bit_width = (first_dword >> 24) & 0xF, + .mem_type = static_cast((first_dword >> 20) & 0xF), + .reg_index = ((first_dword >> 16) & 0xF), + .load_from_reg = ((first_dword >> 12) & 0xF) != 0, + .rel_address = (static_cast(first_dword & 0xFF) << 32) | second_dword, + }; + } break; + case CheatVmOpcodeType::StoreStaticToAddress: { + // 6T0RIor0 VVVVVVVV VVVVVVVV + // Read additional words. + opcode.opcode = StoreStaticToAddressOpcode{ + .bit_width = (first_dword >> 24) & 0xF, + .reg_index = (first_dword >> 16) & 0xF, + .increment_reg = ((first_dword >> 12) & 0xF) != 0, + .add_offset_reg = ((first_dword >> 8) & 0xF) != 0, + .offset_reg_index = (first_dword >> 4) & 0xF, + .value = (static_cast(GetNextDword()) << 32) | GetNextDword(), + }; + } break; + case CheatVmOpcodeType::PerformArithmeticStatic: { + // 7T0RC000 VVVVVVVV + // Read additional words. + opcode.opcode = PerformArithmeticStaticOpcode{ + .bit_width = (first_dword >> 24) & 0xF, + .reg_index = ((first_dword >> 16) & 0xF), + .math_type = static_cast((first_dword >> 12) & 0xF), + .value = GetNextDword(), + }; + } break; + case CheatVmOpcodeType::BeginKeypressConditionalBlock: { + // 8kkkkkkk + // Just parse the mask. + opcode.opcode = BeginKeypressConditionalOpcode{ + .key_mask = first_dword & 0x0FFFFFFF, + }; + } break; + case CheatVmOpcodeType::PerformArithmeticRegister: { + // 9TCRSIs0 (VVVVVVVV (VVVVVVVV)) + PerformArithmeticRegisterOpcode perform_math_reg{ + .bit_width = (first_dword >> 24) & 0xF, + .math_type = static_cast((first_dword >> 20) & 0xF), + .dst_reg_index = (first_dword >> 16) & 0xF, + .src_reg_1_index = (first_dword >> 12) & 0xF, + .src_reg_2_index = 0, + .has_immediate = ((first_dword >> 8) & 0xF) != 0, + .value = {}, + }; + if (perform_math_reg.has_immediate) { + perform_math_reg.src_reg_2_index = 0; + perform_math_reg.value = GetNextVmInt(perform_math_reg.bit_width); + } else { + perform_math_reg.src_reg_2_index = ((first_dword >> 4) & 0xF); + } + opcode.opcode = perform_math_reg; + } break; + case CheatVmOpcodeType::StoreRegisterToAddress: { + // ATSRIOxa (aaaaaaaa) + // A = opcode 10 + // T = bit width + // S = src register index + // R = address register index + // I = 1 if increment address register, 0 if not increment address register + // O = offset type, 0 = None, 1 = Register, 2 = Immediate, 3 = Memory Region, + // 4 = Memory Region + Relative Address (ignore address register), 5 = Memory Region + + // Relative Address + // x = offset register (for offset type 1), memory type (for offset type 3) + // a = relative address (for offset type 2+3) + StoreRegisterToAddressOpcode str_register{ + .bit_width = (first_dword >> 24) & 0xF, + .str_reg_index = (first_dword >> 20) & 0xF, + .addr_reg_index = (first_dword >> 16) & 0xF, + .increment_reg = ((first_dword >> 12) & 0xF) != 0, + .ofs_type = static_cast(((first_dword >> 8) & 0xF)), + .mem_type = MemoryAccessType::MainNso, + .ofs_reg_index = (first_dword >> 4) & 0xF, + .rel_address = 0, + }; + switch (str_register.ofs_type) { + case StoreRegisterOffsetType::None: + case StoreRegisterOffsetType::Reg: + // Nothing more to do + break; + case StoreRegisterOffsetType::Imm: + str_register.rel_address = (static_cast(first_dword & 0xF) << 32) | GetNextDword(); + break; + case StoreRegisterOffsetType::MemReg: + str_register.mem_type = static_cast((first_dword >> 4) & 0xF); + break; + case StoreRegisterOffsetType::MemImm: + case StoreRegisterOffsetType::MemImmReg: + str_register.mem_type = static_cast((first_dword >> 4) & 0xF); + str_register.rel_address = (static_cast(first_dword & 0xF) << 32) | GetNextDword(); + break; + default: + str_register.ofs_type = StoreRegisterOffsetType::None; + break; + } + opcode.opcode = str_register; + } break; + case CheatVmOpcodeType::BeginRegisterConditionalBlock: { + // C0TcSX## + // C0TcS0Ma aaaaaaaa + // C0TcS1Mr + // C0TcS2Ra aaaaaaaa + // C0TcS3Rr + // C0TcS400 VVVVVVVV (VVVVVVVV) + // C0TcS5X0 + // C0 = opcode 0xC0 + // T = bit width + // c = condition type. + // S = source register. + // X = value operand type, 0 = main/heap with relative offset, 1 = main/heap with offset + // register, + // 2 = register with relative offset, 3 = register with offset register, 4 = static + // value, 5 = other register. + // M = memory type. + // R = address register. + // a = relative address. + // r = offset register. + // X = other register. + // V = value. + + BeginRegisterConditionalOpcode begin_reg_cond{ + .bit_width = (first_dword >> 20) & 0xF, + .cond_type = static_cast((first_dword >> 16) & 0xF), + .val_reg_index = (first_dword >> 12) & 0xF, + .comp_type = static_cast((first_dword >> 8) & 0xF), + .mem_type = MemoryAccessType::MainNso, + .addr_reg_index = 0, + .other_reg_index = 0, + .ofs_reg_index = 0, + .rel_address = 0, + .value = {}, + }; + + switch (begin_reg_cond.comp_type) { + case CompareRegisterValueType::StaticValue: + begin_reg_cond.value = GetNextVmInt(begin_reg_cond.bit_width); + break; + case CompareRegisterValueType::OtherRegister: + begin_reg_cond.other_reg_index = ((first_dword >> 4) & 0xF); + break; + case CompareRegisterValueType::MemoryRelAddr: + begin_reg_cond.mem_type = static_cast((first_dword >> 4) & 0xF); + begin_reg_cond.rel_address = + (static_cast(first_dword & 0xF) << 32) | GetNextDword(); + break; + case CompareRegisterValueType::MemoryOfsReg: + begin_reg_cond.mem_type = static_cast((first_dword >> 4) & 0xF); + begin_reg_cond.ofs_reg_index = (first_dword & 0xF); + break; + case CompareRegisterValueType::RegisterRelAddr: + begin_reg_cond.addr_reg_index = (first_dword >> 4) & 0xF; + begin_reg_cond.rel_address = + (static_cast(first_dword & 0xF) << 32) | GetNextDword(); + break; + case CompareRegisterValueType::RegisterOfsReg: + begin_reg_cond.addr_reg_index = (first_dword >> 4) & 0xF; + begin_reg_cond.ofs_reg_index = first_dword & 0xF; + break; + } + opcode.opcode = begin_reg_cond; + } break; + case CheatVmOpcodeType::SaveRestoreRegister: { + // C10D0Sx0 + // C1 = opcode 0xC1 + // D = destination index. + // S = source index. + // x = 3 if clearing reg, 2 if clearing saved value, 1 if saving a register, 0 if restoring + // a register. + // NOTE: If we add more save slots later, current encoding is backwards compatible. + opcode.opcode = SaveRestoreRegisterOpcode{ + .dst_index = (first_dword >> 16) & 0xF, + .src_index = (first_dword >> 8) & 0xF, + .op_type = static_cast((first_dword >> 4) & 0xF), + }; + } break; + case CheatVmOpcodeType::SaveRestoreRegisterMask: { + // C2x0XXXX + // C2 = opcode 0xC2 + // x = 3 if clearing reg, 2 if clearing saved value, 1 if saving, 0 if restoring. + // X = 16-bit bitmask, bit i --> save or restore register i. + SaveRestoreRegisterMaskOpcode save_restore_regmask{ + .op_type = static_cast((first_dword >> 20) & 0xF), + .should_operate = {}, + }; + for (std::size_t i = 0; i < NumRegisters; i++) { + save_restore_regmask.should_operate[i] = (first_dword & (1U << i)) != 0; + } + opcode.opcode = save_restore_regmask; + } break; + case CheatVmOpcodeType::ReadWriteStaticRegister: { + // C3000XXx + // C3 = opcode 0xC3. + // XX = static register index. + // x = register index. + opcode.opcode = ReadWriteStaticRegisterOpcode{ + .static_idx = (first_dword >> 4) & 0xFF, + .idx = first_dword & 0xF, + }; + } break; + case CheatVmOpcodeType::PauseProcess: { + /* FF0????? */ + /* FF0 = opcode 0xFF0 */ + /* Pauses the current process. */ + opcode.opcode = PauseProcessOpcode{}; + } break; + case CheatVmOpcodeType::ResumeProcess: { + /* FF0????? */ + /* FF0 = opcode 0xFF0 */ + /* Pauses the current process. */ + opcode.opcode = ResumeProcessOpcode{}; + } break; + case CheatVmOpcodeType::DebugLog: { + // FFFTIX## + // FFFTI0Ma aaaaaaaa + // FFFTI1Mr + // FFFTI2Ra aaaaaaaa + // FFFTI3Rr + // FFFTI4X0 + // FFF = opcode 0xFFF + // T = bit width. + // I = log id. + // X = value operand type, 0 = main/heap with relative offset, 1 = main/heap with offset + // register, + // 2 = register with relative offset, 3 = register with offset register, 4 = register + // value. + // M = memory type. + // R = address register. + // a = relative address. + // r = offset register. + // X = value register. + DebugLogOpcode debug_log{ + .bit_width = (first_dword >> 16) & 0xF, + .log_id = (first_dword >> 12) & 0xF, + .val_type = static_cast((first_dword >> 8) & 0xF), + .mem_type = MemoryAccessType::MainNso, + .addr_reg_index = 0, + .val_reg_index = 0, + .ofs_reg_index = 0, + .rel_address = 0, + }; + + switch (debug_log.val_type) { + case DebugLogValueType::RegisterValue: + debug_log.val_reg_index = (first_dword >> 4) & 0xF; + break; + case DebugLogValueType::MemoryRelAddr: + debug_log.mem_type = static_cast((first_dword >> 4) & 0xF); + debug_log.rel_address = (static_cast(first_dword & 0xF) << 32) | GetNextDword(); + break; + case DebugLogValueType::MemoryOfsReg: + debug_log.mem_type = static_cast((first_dword >> 4) & 0xF); + debug_log.ofs_reg_index = first_dword & 0xF; + break; + case DebugLogValueType::RegisterRelAddr: + debug_log.addr_reg_index = (first_dword >> 4) & 0xF; + debug_log.rel_address = (static_cast(first_dword & 0xF) << 32) | GetNextDword(); + break; + case DebugLogValueType::RegisterOfsReg: + debug_log.addr_reg_index = (first_dword >> 4) & 0xF; + debug_log.ofs_reg_index = first_dword & 0xF; + break; + } + opcode.opcode = debug_log; + } break; + case CheatVmOpcodeType::ExtendedWidth: + case CheatVmOpcodeType::DoubleExtendedWidth: + default: + // Unrecognized instruction cannot be decoded. + valid = false; + opcode.opcode = UnrecognizedInstruction{opcode_type}; + break; + } + + // End decoding. + return valid; + } + + void CheatVirtualMachine::SkipConditionalBlock(bool is_if) { + if (condition_depth > 0) { + // We want to continue until we're out of the current block. + const std::size_t desired_depth = condition_depth - 1; + + CheatVmOpcode skip_opcode{}; + while (condition_depth > desired_depth && DecodeNextOpcode(skip_opcode)) { + // Decode instructions until we see end of the current conditional block. + // NOTE: This is broken in gateway's implementation. + // Gateway currently checks for "0x2" instead of "0x20000000" + // In addition, they do a linear scan instead of correctly decoding opcodes. + // This causes issues if "0x2" appears as an immediate in the conditional block... + + // We also support nesting of conditional blocks, and Gateway does not. + if (skip_opcode.begin_conditional_block) { + condition_depth++; + } else if (auto end_cond = std::get_if(&skip_opcode.opcode)) { + if (!end_cond->is_else) { + condition_depth--; + } else if (is_if && condition_depth - 1 == desired_depth) { + break; + } + } + } + } else { + // Skipping, but condition_depth = 0. + // This is an error condition. + // However, I don't actually believe it is possible for this to happen. + // I guess we'll throw a fatal error here, so as to encourage me to fix the VM + // in the event that someone triggers it? I don't know how you'd do that. + UNREACHABLE_MSG("Invalid condition depth in DMNT Cheat VM"); + } + } + + u64 CheatVirtualMachine::GetVmInt(VmInt value, u32 bit_width) { + switch (bit_width) { + case 1: + return value.bit8; + case 2: + return value.bit16; + case 4: + return value.bit32; + case 8: + return value.bit64; + default: + // Invalid bit width -> return 0. + return 0; + } + } + + u64 CheatVirtualMachine::GetCheatProcessAddress(const CheatProcessMetadata& metadata, + MemoryAccessType mem_type, u64 rel_address) { + switch (mem_type) { + case MemoryAccessType::MainNso: + default: + return metadata.main_nso_extents.base + rel_address; + case MemoryAccessType::Heap: + return metadata.heap_extents.base + rel_address; + case MemoryAccessType::Alias: + return metadata.alias_extents.base + rel_address; + case MemoryAccessType::Aslr: + return metadata.aslr_extents.base + rel_address; + } + } + + void CheatVirtualMachine::ResetState() { + registers.fill(0); + saved_values.fill(0); + loop_tops.fill(0); + instruction_ptr = 0; + condition_depth = 0; + decode_success = true; + } + + bool CheatVirtualMachine::LoadProgram(std::span entries) { + // Reset opcode count. + num_opcodes = 0; + + 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; + } + + for (std::size_t n = 0; n < entries[i].definition.num_opcodes; n++) { + program[num_opcodes++] = entries[i].definition.opcodes[n]; + } + } + } + + return true; + } + + void CheatVirtualMachine::Execute(const CheatProcessMetadata& metadata) { + CheatVmOpcode cur_opcode{}; + + // Get Keys down. + u64 kDown = manager.HidKeysDown(); + + manager.CommandLog("Started VM execution."); + manager.CommandLog(fmt::format("Main NSO: {:012X}", metadata.main_nso_extents.base)); + manager.CommandLog(fmt::format("Heap: {:012X}", metadata.main_nso_extents.base)); + manager.CommandLog(fmt::format("Keys Down: {:08X}", static_cast(kDown & 0x0FFFFFFF))); + + // Clear VM state. + ResetState(); + + // Loop until program finishes. + while (DecodeNextOpcode(cur_opcode)) { + manager.CommandLog( + fmt::format("Instruction Ptr: {:04X}", static_cast(instruction_ptr))); + + for (std::size_t i = 0; i < NumRegisters; i++) { + manager.CommandLog(fmt::format("Registers[{:02X}]: {:016X}", i, registers[i])); + } + + for (std::size_t i = 0; i < NumRegisters; i++) { + manager.CommandLog(fmt::format("SavedRegs[{:02X}]: {:016X}", i, saved_values[i])); + } + LogOpcode(cur_opcode); + + // Increment conditional depth, if relevant. + if (cur_opcode.begin_conditional_block) { + condition_depth++; + } + + if (auto store_static = std::get_if(&cur_opcode.opcode)) { + // Calculate address, write value to memory. + u64 dst_address = GetCheatProcessAddress(metadata, store_static->mem_type, + store_static->rel_address + + registers[store_static->offset_register]); + u64 dst_value = GetVmInt(store_static->value, store_static->bit_width); + switch (store_static->bit_width) { + case 1: + case 2: + case 4: + case 8: + manager.WriteCheatProcessMemoryUnsafe(dst_address, &dst_value, + store_static->bit_width); + break; + } + } else if (auto begin_cond = std::get_if(&cur_opcode.opcode)) { + // Read value from memory. + u64 src_address = + GetCheatProcessAddress(metadata, begin_cond->mem_type, begin_cond->rel_address); + u64 src_value = 0; + switch (begin_cond->bit_width) { + case 1: + case 2: + case 4: + case 8: + manager.ReadCheatProcessMemoryUnsafe(src_address, &src_value, + begin_cond->bit_width); + break; + } + // Check against condition. + u64 cond_value = GetVmInt(begin_cond->value, begin_cond->bit_width); + bool cond_met = false; + switch (begin_cond->cond_type) { + case ConditionalComparisonType::GT: + cond_met = src_value > cond_value; + break; + case ConditionalComparisonType::GE: + cond_met = src_value >= cond_value; + break; + case ConditionalComparisonType::LT: + cond_met = src_value < cond_value; + break; + case ConditionalComparisonType::LE: + cond_met = src_value <= cond_value; + break; + case ConditionalComparisonType::EQ: + cond_met = src_value == cond_value; + break; + case ConditionalComparisonType::NE: + cond_met = src_value != cond_value; + break; + } + // Skip conditional block if condition not met. + if (!cond_met) { + SkipConditionalBlock(true); + } + } else if (auto end_cond = std::get_if(&cur_opcode.opcode)) { + if (end_cond->is_else) { + /* Skip to the end of the conditional block. */ + this->SkipConditionalBlock(false); + } else { + /* Decrement the condition depth. */ + /* We will assume, graciously, that mismatched conditional block ends are a nop. */ + if (condition_depth > 0) { + condition_depth--; + } + } + } else if (auto ctrl_loop = std::get_if(&cur_opcode.opcode)) { + if (ctrl_loop->start_loop) { + // Start a loop. + registers[ctrl_loop->reg_index] = ctrl_loop->num_iters; + loop_tops[ctrl_loop->reg_index] = instruction_ptr; + } else { + // End a loop. + registers[ctrl_loop->reg_index]--; + if (registers[ctrl_loop->reg_index] != 0) { + instruction_ptr = loop_tops[ctrl_loop->reg_index]; + } + } + } else if (auto ldr_static = std::get_if(&cur_opcode.opcode)) { + // Set a register to a static value. + registers[ldr_static->reg_index] = ldr_static->value; + } else if (auto ldr_memory = std::get_if(&cur_opcode.opcode)) { + // Choose source address. + u64 src_address; + if (ldr_memory->load_from_reg) { + src_address = registers[ldr_memory->reg_index] + ldr_memory->rel_address; + } else { + src_address = + GetCheatProcessAddress(metadata, ldr_memory->mem_type, ldr_memory->rel_address); + } + // Read into register. Gateway only reads on valid bitwidth. + switch (ldr_memory->bit_width) { + case 1: + case 2: + case 4: + case 8: + manager.ReadCheatProcessMemoryUnsafe(src_address, ®isters[ldr_memory->reg_index], + ldr_memory->bit_width); + break; + } + } else if (auto str_static = std::get_if(&cur_opcode.opcode)) { + // Calculate address. + u64 dst_address = registers[str_static->reg_index]; + u64 dst_value = str_static->value; + if (str_static->add_offset_reg) { + dst_address += registers[str_static->offset_reg_index]; + } + // Write value to memory. Gateway only writes on valid bitwidth. + switch (str_static->bit_width) { + case 1: + case 2: + case 4: + case 8: + manager.WriteCheatProcessMemoryUnsafe(dst_address, &dst_value, + str_static->bit_width); + break; + } + // Increment register if relevant. + if (str_static->increment_reg) { + registers[str_static->reg_index] += str_static->bit_width; + } + } else if (auto perform_math_static = + std::get_if(&cur_opcode.opcode)) { + // Do requested math. + switch (perform_math_static->math_type) { + case RegisterArithmeticType::Addition: + registers[perform_math_static->reg_index] += + static_cast(perform_math_static->value); + break; + case RegisterArithmeticType::Subtraction: + registers[perform_math_static->reg_index] -= + static_cast(perform_math_static->value); + break; + case RegisterArithmeticType::Multiplication: + registers[perform_math_static->reg_index] *= + static_cast(perform_math_static->value); + break; + case RegisterArithmeticType::LeftShift: + registers[perform_math_static->reg_index] <<= + static_cast(perform_math_static->value); + break; + case RegisterArithmeticType::RightShift: + registers[perform_math_static->reg_index] >>= + static_cast(perform_math_static->value); + break; + default: + // Do not handle extensions here. + break; + } + // Apply bit width. + switch (perform_math_static->bit_width) { + case 1: + registers[perform_math_static->reg_index] = + static_cast(registers[perform_math_static->reg_index]); + break; + case 2: + registers[perform_math_static->reg_index] = + static_cast(registers[perform_math_static->reg_index]); + break; + case 4: + registers[perform_math_static->reg_index] = + static_cast(registers[perform_math_static->reg_index]); + break; + case 8: + registers[perform_math_static->reg_index] = + static_cast(registers[perform_math_static->reg_index]); + break; + } + } else if (auto begin_keypress_cond = + std::get_if(&cur_opcode.opcode)) { + // Check for keypress. + if ((begin_keypress_cond->key_mask & kDown) != begin_keypress_cond->key_mask) { + // Keys not pressed. Skip conditional block. + SkipConditionalBlock(true); + } + } else if (auto perform_math_reg = + std::get_if(&cur_opcode.opcode)) { + const u64 operand_1_value = registers[perform_math_reg->src_reg_1_index]; + const u64 operand_2_value = + perform_math_reg->has_immediate + ? GetVmInt(perform_math_reg->value, perform_math_reg->bit_width) + : registers[perform_math_reg->src_reg_2_index]; + + u64 res_val = 0; + // Do requested math. + switch (perform_math_reg->math_type) { + case RegisterArithmeticType::Addition: + res_val = operand_1_value + operand_2_value; + break; + case RegisterArithmeticType::Subtraction: + res_val = operand_1_value - operand_2_value; + break; + case RegisterArithmeticType::Multiplication: + res_val = operand_1_value * operand_2_value; + break; + case RegisterArithmeticType::LeftShift: + res_val = operand_1_value << operand_2_value; + break; + case RegisterArithmeticType::RightShift: + res_val = operand_1_value >> operand_2_value; + break; + case RegisterArithmeticType::LogicalAnd: + res_val = operand_1_value & operand_2_value; + break; + case RegisterArithmeticType::LogicalOr: + res_val = operand_1_value | operand_2_value; + break; + case RegisterArithmeticType::LogicalNot: + res_val = ~operand_1_value; + break; + case RegisterArithmeticType::LogicalXor: + res_val = operand_1_value ^ operand_2_value; + break; + case RegisterArithmeticType::None: + res_val = operand_1_value; + break; + } + + // Apply bit width. + switch (perform_math_reg->bit_width) { + case 1: + res_val = static_cast(res_val); + break; + case 2: + res_val = static_cast(res_val); + break; + case 4: + res_val = static_cast(res_val); + break; + case 8: + res_val = static_cast(res_val); + break; + } + + // Save to register. + registers[perform_math_reg->dst_reg_index] = res_val; + } else if (auto str_register = + std::get_if(&cur_opcode.opcode)) { + // Calculate address. + u64 dst_value = registers[str_register->str_reg_index]; + u64 dst_address = registers[str_register->addr_reg_index]; + switch (str_register->ofs_type) { + case StoreRegisterOffsetType::None: + // Nothing more to do + break; + case StoreRegisterOffsetType::Reg: + dst_address += registers[str_register->ofs_reg_index]; + break; + case StoreRegisterOffsetType::Imm: + dst_address += str_register->rel_address; + break; + case StoreRegisterOffsetType::MemReg: + dst_address = GetCheatProcessAddress(metadata, str_register->mem_type, + registers[str_register->addr_reg_index]); + break; + case StoreRegisterOffsetType::MemImm: + dst_address = GetCheatProcessAddress(metadata, str_register->mem_type, + str_register->rel_address); + break; + case StoreRegisterOffsetType::MemImmReg: + dst_address = GetCheatProcessAddress(metadata, str_register->mem_type, + registers[str_register->addr_reg_index] + + str_register->rel_address); + break; + } + + // Write value to memory. Write only on valid bitwidth. + switch (str_register->bit_width) { + case 1: + case 2: + case 4: + case 8: + manager.WriteCheatProcessMemoryUnsafe(dst_address, &dst_value, + str_register->bit_width); + break; + } + + // Increment register if relevant. + if (str_register->increment_reg) { + registers[str_register->addr_reg_index] += str_register->bit_width; + } + } else if (auto begin_reg_cond = + std::get_if(&cur_opcode.opcode)) { + // Get value from register. + u64 src_value = 0; + switch (begin_reg_cond->bit_width) { + case 1: + src_value = static_cast(registers[begin_reg_cond->val_reg_index] & 0xFFul); + break; + case 2: + src_value = static_cast(registers[begin_reg_cond->val_reg_index] & 0xFFFFul); + break; + case 4: + src_value = + static_cast(registers[begin_reg_cond->val_reg_index] & 0xFFFFFFFFul); + break; + case 8: + src_value = static_cast(registers[begin_reg_cond->val_reg_index] & + 0xFFFFFFFFFFFFFFFFul); + break; + } + + // Read value from memory. + u64 cond_value = 0; + if (begin_reg_cond->comp_type == CompareRegisterValueType::StaticValue) { + cond_value = GetVmInt(begin_reg_cond->value, begin_reg_cond->bit_width); + } else if (begin_reg_cond->comp_type == CompareRegisterValueType::OtherRegister) { + switch (begin_reg_cond->bit_width) { + case 1: + cond_value = + static_cast(registers[begin_reg_cond->other_reg_index] & 0xFFul); + break; + case 2: + cond_value = + static_cast(registers[begin_reg_cond->other_reg_index] & 0xFFFFul); + break; + case 4: + cond_value = + static_cast(registers[begin_reg_cond->other_reg_index] & 0xFFFFFFFFul); + break; + case 8: + cond_value = static_cast(registers[begin_reg_cond->other_reg_index] & + 0xFFFFFFFFFFFFFFFFul); + break; + } + } else { + u64 cond_address = 0; + switch (begin_reg_cond->comp_type) { + case CompareRegisterValueType::MemoryRelAddr: + cond_address = GetCheatProcessAddress(metadata, begin_reg_cond->mem_type, + begin_reg_cond->rel_address); + break; + case CompareRegisterValueType::MemoryOfsReg: + cond_address = GetCheatProcessAddress(metadata, begin_reg_cond->mem_type, + registers[begin_reg_cond->ofs_reg_index]); + break; + case CompareRegisterValueType::RegisterRelAddr: + cond_address = + registers[begin_reg_cond->addr_reg_index] + begin_reg_cond->rel_address; + break; + case CompareRegisterValueType::RegisterOfsReg: + cond_address = registers[begin_reg_cond->addr_reg_index] + + registers[begin_reg_cond->ofs_reg_index]; + break; + default: + break; + } + switch (begin_reg_cond->bit_width) { + case 1: + case 2: + case 4: + case 8: + manager.ReadCheatProcessMemoryUnsafe(cond_address, &cond_value, + begin_reg_cond->bit_width); + break; + } + } + + // Check against condition. + bool cond_met = false; + switch (begin_reg_cond->cond_type) { + case ConditionalComparisonType::GT: + cond_met = src_value > cond_value; + break; + case ConditionalComparisonType::GE: + cond_met = src_value >= cond_value; + break; + case ConditionalComparisonType::LT: + cond_met = src_value < cond_value; + break; + case ConditionalComparisonType::LE: + cond_met = src_value <= cond_value; + break; + case ConditionalComparisonType::EQ: + cond_met = src_value == cond_value; + break; + case ConditionalComparisonType::NE: + cond_met = src_value != cond_value; + break; + } + + // Skip conditional block if condition not met. + if (!cond_met) { + SkipConditionalBlock(true); + } + } else if (auto save_restore_reg = + std::get_if(&cur_opcode.opcode)) { + // Save or restore a register. + switch (save_restore_reg->op_type) { + case SaveRestoreRegisterOpType::ClearRegs: + registers[save_restore_reg->dst_index] = 0ul; + break; + case SaveRestoreRegisterOpType::ClearSaved: + saved_values[save_restore_reg->dst_index] = 0ul; + break; + case SaveRestoreRegisterOpType::Save: + saved_values[save_restore_reg->dst_index] = registers[save_restore_reg->src_index]; + break; + case SaveRestoreRegisterOpType::Restore: + default: + registers[save_restore_reg->dst_index] = saved_values[save_restore_reg->src_index]; + break; + } + } else if (auto save_restore_regmask = + std::get_if(&cur_opcode.opcode)) { + // Save or restore register mask. + u64* src; + u64* dst; + switch (save_restore_regmask->op_type) { + case SaveRestoreRegisterOpType::ClearSaved: + case SaveRestoreRegisterOpType::Save: + src = registers.data(); + dst = saved_values.data(); + break; + case SaveRestoreRegisterOpType::ClearRegs: + case SaveRestoreRegisterOpType::Restore: + default: + src = saved_values.data(); + dst = registers.data(); + break; + } + for (std::size_t i = 0; i < NumRegisters; i++) { + if (save_restore_regmask->should_operate[i]) { + switch (save_restore_regmask->op_type) { + case SaveRestoreRegisterOpType::ClearSaved: + case SaveRestoreRegisterOpType::ClearRegs: + dst[i] = 0ul; + break; + case SaveRestoreRegisterOpType::Save: + case SaveRestoreRegisterOpType::Restore: + default: + dst[i] = src[i]; + break; + } + } + } + } else if (auto rw_static_reg = + std::get_if(&cur_opcode.opcode)) { + if (rw_static_reg->static_idx < NumReadableStaticRegisters) { + // Load a register with a static register. + registers[rw_static_reg->idx] = static_registers[rw_static_reg->static_idx]; + } else { + // Store a register to a static register. + static_registers[rw_static_reg->static_idx] = registers[rw_static_reg->idx]; + } + } else if (std::holds_alternative(cur_opcode.opcode)) { + manager.PauseCheatProcessUnsafe(); + } else if (std::holds_alternative(cur_opcode.opcode)) { + manager.ResumeCheatProcessUnsafe(); + } else if (auto debug_log = std::get_if(&cur_opcode.opcode)) { + // Read value from memory. + u64 log_value = 0; + if (debug_log->val_type == DebugLogValueType::RegisterValue) { + switch (debug_log->bit_width) { + case 1: + log_value = static_cast(registers[debug_log->val_reg_index] & 0xFFul); + break; + case 2: + log_value = static_cast(registers[debug_log->val_reg_index] & 0xFFFFul); + break; + case 4: + log_value = + static_cast(registers[debug_log->val_reg_index] & 0xFFFFFFFFul); + break; + case 8: + log_value = static_cast(registers[debug_log->val_reg_index] & + 0xFFFFFFFFFFFFFFFFul); + break; + } + } else { + u64 val_address = 0; + switch (debug_log->val_type) { + case DebugLogValueType::MemoryRelAddr: + val_address = GetCheatProcessAddress(metadata, debug_log->mem_type, + debug_log->rel_address); + break; + case DebugLogValueType::MemoryOfsReg: + val_address = GetCheatProcessAddress(metadata, debug_log->mem_type, + registers[debug_log->ofs_reg_index]); + break; + case DebugLogValueType::RegisterRelAddr: + val_address = registers[debug_log->addr_reg_index] + debug_log->rel_address; + break; + case DebugLogValueType::RegisterOfsReg: + val_address = + registers[debug_log->addr_reg_index] + registers[debug_log->ofs_reg_index]; + break; + default: + break; + } + switch (debug_log->bit_width) { + case 1: + case 2: + case 4: + case 8: + manager.ReadCheatProcessMemoryUnsafe(val_address, &log_value, + debug_log->bit_width); + break; + } + } + + // Log value. + DebugLog(debug_log->log_id, log_value); + } + } + } +} // namespace Service::DMNT diff --git a/src/core/hle/service/dmnt/cheat_virtual_machine.h b/src/core/hle/service/dmnt/cheat_virtual_machine.h new file mode 100644 index 0000000000..8860764701 --- /dev/null +++ b/src/core/hle/service/dmnt/cheat_virtual_machine.h @@ -0,0 +1,323 @@ +// SPDX-FileCopyrightText: Copyright 2025 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" +#include "core/hle/service/dmnt/dmnt_types.h" + +namespace Service::DMNT { + class CheatProcessManager; + + enum class CheatVmOpcodeType : u32 { + StoreStatic = 0, + BeginConditionalBlock = 1, + EndConditionalBlock = 2, + ControlLoop = 3, + LoadRegisterStatic = 4, + LoadRegisterMemory = 5, + StoreStaticToAddress = 6, + PerformArithmeticStatic = 7, + BeginKeypressConditionalBlock = 8, + + // These are not implemented by Gateway's VM. + PerformArithmeticRegister = 9, + StoreRegisterToAddress = 10, + Reserved11 = 11, + + // This is a meta entry, and not a real opcode. + // This is to facilitate multi-nybble instruction decoding. + ExtendedWidth = 12, + + // Extended width opcodes. + BeginRegisterConditionalBlock = 0xC0, + SaveRestoreRegister = 0xC1, + SaveRestoreRegisterMask = 0xC2, + ReadWriteStaticRegister = 0xC3, + + // This is a meta entry, and not a real opcode. + // This is to facilitate multi-nybble instruction decoding. + DoubleExtendedWidth = 0xF0, + + // Double-extended width opcodes. + PauseProcess = 0xFF0, + ResumeProcess = 0xFF1, + DebugLog = 0xFFF, + }; + + enum class MemoryAccessType : u32 { + MainNso = 0, + Heap = 1, + Alias = 2, + Aslr = 3, + }; + + enum class ConditionalComparisonType : u32 { + GT = 1, + GE = 2, + LT = 3, + LE = 4, + EQ = 5, + NE = 6, + }; + + enum class RegisterArithmeticType : u32 { + Addition = 0, + Subtraction = 1, + Multiplication = 2, + LeftShift = 3, + RightShift = 4, + + // These are not supported by Gateway's VM. + LogicalAnd = 5, + LogicalOr = 6, + LogicalNot = 7, + LogicalXor = 8, + + None = 9, + }; + + enum class StoreRegisterOffsetType : u32 { + None = 0, + Reg = 1, + Imm = 2, + MemReg = 3, + MemImm = 4, + MemImmReg = 5, + }; + + enum class CompareRegisterValueType : u32 { + MemoryRelAddr = 0, + MemoryOfsReg = 1, + RegisterRelAddr = 2, + RegisterOfsReg = 3, + StaticValue = 4, + OtherRegister = 5, + }; + + enum class SaveRestoreRegisterOpType : u32 { + Restore = 0, + Save = 1, + ClearSaved = 2, + ClearRegs = 3, + }; + + enum class DebugLogValueType : u32 { + MemoryRelAddr = 0, + MemoryOfsReg = 1, + RegisterRelAddr = 2, + RegisterOfsReg = 3, + RegisterValue = 4, + }; + + union VmInt { + u8 bit8; + u16 bit16; + u32 bit32; + u64 bit64; + }; + + struct StoreStaticOpcode { + u32 bit_width{}; + MemoryAccessType mem_type{}; + u32 offset_register{}; + u64 rel_address{}; + VmInt value{}; + }; + + struct BeginConditionalOpcode { + u32 bit_width{}; + MemoryAccessType mem_type{}; + ConditionalComparisonType cond_type{}; + u64 rel_address{}; + VmInt value{}; + }; + + struct EndConditionalOpcode { + bool is_else; + }; + + struct ControlLoopOpcode { + bool start_loop{}; + u32 reg_index{}; + u32 num_iters{}; + }; + + struct LoadRegisterStaticOpcode { + u32 reg_index{}; + u64 value{}; + }; + + struct LoadRegisterMemoryOpcode { + u32 bit_width{}; + MemoryAccessType mem_type{}; + u32 reg_index{}; + bool load_from_reg{}; + u64 rel_address{}; + }; + + struct StoreStaticToAddressOpcode { + u32 bit_width{}; + u32 reg_index{}; + bool increment_reg{}; + bool add_offset_reg{}; + u32 offset_reg_index{}; + u64 value{}; + }; + + struct PerformArithmeticStaticOpcode { + u32 bit_width{}; + u32 reg_index{}; + RegisterArithmeticType math_type{}; + u32 value{}; + }; + + struct BeginKeypressConditionalOpcode { + u32 key_mask{}; + }; + + struct PerformArithmeticRegisterOpcode { + u32 bit_width{}; + RegisterArithmeticType math_type{}; + u32 dst_reg_index{}; + u32 src_reg_1_index{}; + u32 src_reg_2_index{}; + bool has_immediate{}; + VmInt value{}; + }; + + struct StoreRegisterToAddressOpcode { + u32 bit_width{}; + u32 str_reg_index{}; + u32 addr_reg_index{}; + bool increment_reg{}; + StoreRegisterOffsetType ofs_type{}; + MemoryAccessType mem_type{}; + u32 ofs_reg_index{}; + u64 rel_address{}; + }; + + struct BeginRegisterConditionalOpcode { + u32 bit_width{}; + ConditionalComparisonType cond_type{}; + u32 val_reg_index{}; + CompareRegisterValueType comp_type{}; + MemoryAccessType mem_type{}; + u32 addr_reg_index{}; + u32 other_reg_index{}; + u32 ofs_reg_index{}; + u64 rel_address{}; + VmInt value{}; + }; + + struct SaveRestoreRegisterOpcode { + u32 dst_index{}; + u32 src_index{}; + SaveRestoreRegisterOpType op_type{}; + }; + + struct SaveRestoreRegisterMaskOpcode { + SaveRestoreRegisterOpType op_type{}; + std::array should_operate{}; + }; + + struct ReadWriteStaticRegisterOpcode { + u32 static_idx{}; + u32 idx{}; + }; + + struct PauseProcessOpcode {}; + + struct ResumeProcessOpcode {}; + + struct DebugLogOpcode { + u32 bit_width{}; + u32 log_id{}; + DebugLogValueType val_type{}; + MemoryAccessType mem_type{}; + u32 addr_reg_index{}; + u32 val_reg_index{}; + u32 ofs_reg_index{}; + u64 rel_address{}; + }; + + struct UnrecognizedInstruction { + CheatVmOpcodeType opcode{}; + }; + + struct CheatVmOpcode { + bool begin_conditional_block{}; + std::variant + opcode{}; + }; + + class CheatVirtualMachine { + public: + 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; + static constexpr std::size_t NumStaticRegisters = + NumReadableStaticRegisters + NumWritableStaticRegisters; + + explicit CheatVirtualMachine(CheatProcessManager& cheat_manager); + ~CheatVirtualMachine(); + + std::size_t GetProgramSize() const { + return this->num_opcodes; + } + + bool LoadProgram(std::span cheats); + void Execute(const CheatProcessMetadata& metadata); + + u64 GetStaticRegister(std::size_t register_index) const { + return static_registers[register_index]; + } + + void SetStaticRegister(std::size_t register_index, u64 value) { + static_registers[register_index] = value; + } + + void ResetStaticRegisters() { + static_registers = {}; + } + + private: + bool DecodeNextOpcode(CheatVmOpcode& out); + void SkipConditionalBlock(bool is_if); + void ResetState(); + + // For implementing the DebugLog opcode. + void DebugLog(u32 log_id, u64 value) const; + + void LogOpcode(const CheatVmOpcode& opcode) const; + + static u64 GetVmInt(VmInt value, u32 bit_width); + static u64 GetCheatProcessAddress(const CheatProcessMetadata& metadata, + MemoryAccessType mem_type, u64 rel_address); + + CheatProcessManager& manager; + + 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::array registers{}; + std::array saved_values{}; + std::array static_registers{}; + std::array loop_tops{}; + }; +}// namespace Service::DMNT diff --git a/src/core/hle/service/dmnt/dmnt.cpp b/src/core/hle/service/dmnt/dmnt.cpp new file mode 100644 index 0000000000..373ef83b9e --- /dev/null +++ b/src/core/hle/service/dmnt/dmnt.cpp @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "core/core.h" +#include "core/hle/service/dmnt/cheat_interface.h" +#include "core/hle/service/dmnt/cheat_process_manager.h" +#include "core/hle/service/dmnt/cheat_virtual_machine.h" +#include "core/hle/service/dmnt/dmnt.h" +#include "core/hle/service/server_manager.h" + +namespace Service::DMNT { + void LoopProcess(Core::System& system) { + auto server_manager = std::make_unique(system); + + auto& cheat_manager = system.GetCheatManager(); + auto cheat_vm = std::make_unique(cheat_manager); + cheat_manager.SetVirtualMachine(std::move(cheat_vm)); + + server_manager->RegisterNamedService("dmnt:cht", + std::make_shared(system, cheat_manager)); + ServerManager::RunServer(std::move(server_manager)); + } +} // namespace Service::DMNT diff --git a/src/core/hle/service/dmnt/dmnt.h b/src/core/hle/service/dmnt/dmnt.h new file mode 100644 index 0000000000..4c98a5fc0f --- /dev/null +++ b/src/core/hle/service/dmnt/dmnt.h @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +namespace Core { + class System; +}; + +namespace Service::DMNT { + void LoopProcess(Core::System& system); +} // namespace Service::DMNT diff --git a/src/core/hle/service/dmnt/dmnt_results.h b/src/core/hle/service/dmnt/dmnt_results.h new file mode 100644 index 0000000000..ec3859c936 --- /dev/null +++ b/src/core/hle/service/dmnt/dmnt_results.h @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "core/hle/result.h" + +namespace Service::DMNT { + constexpr Result ResultDebuggingDisabled(ErrorModule::DMNT, 2); + + constexpr Result ResultCheatNotAttached(ErrorModule::DMNT, 6500); + constexpr Result ResultCheatNullBuffer(ErrorModule::DMNT, 6501); + constexpr Result ResultCheatInvalidBuffer(ErrorModule::DMNT, 6502); + constexpr Result ResultCheatUnknownId(ErrorModule::DMNT, 6503); + constexpr Result ResultCheatOutOfResource(ErrorModule::DMNT, 6504); + constexpr Result ResultCheatInvalid(ErrorModule::DMNT, 6505); + constexpr Result ResultCheatCannotDisable(ErrorModule::DMNT, 6506); + constexpr Result ResultFrozenAddressInvalidWidth(ErrorModule::DMNT, 6600); + constexpr Result ResultFrozenAddressAlreadyExists(ErrorModule::DMNT, 6601); + constexpr Result ResultFrozenAddressNotFound(ErrorModule::DMNT, 6602); + constexpr Result ResultFrozenAddressOutOfResource(ErrorModule::DMNT, 6603); + constexpr Result ResultVirtualMachineInvalidConditionDepth(ErrorModule::DMNT, 6700); +} // namespace Service::DMNT diff --git a/src/core/hle/service/dmnt/dmnt_types.h b/src/core/hle/service/dmnt/dmnt_types.h new file mode 100644 index 0000000000..60a118200a --- /dev/null +++ b/src/core/hle/service/dmnt/dmnt_types.h @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: Copyright 2025 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 "common/common_types.h" + +namespace Service::DMNT { + struct MemoryRegionExtents { + u64 base{}; + u64 size{}; + }; + static_assert(sizeof(MemoryRegionExtents) == 0x10, "MemoryRegionExtents is an invalid size"); + + struct CheatProcessMetadata { + u64 process_id{}; + u64 program_id{}; + MemoryRegionExtents main_nso_extents{}; + MemoryRegionExtents heap_extents{}; + MemoryRegionExtents alias_extents{}; + MemoryRegionExtents aslr_extents{}; + std::array main_nso_build_id{}; + }; + static_assert(sizeof(CheatProcessMetadata) == 0x70, "CheatProcessMetadata is an invalid size"); + + struct CheatDefinition { + std::array readable_name; + u32 num_opcodes; + std::array opcodes; + }; + static_assert(sizeof(CheatDefinition) == 0x444, "CheatDefinition is an invalid size"); + + struct CheatEntry { + bool enabled; + u32 cheat_id; + CheatDefinition definition; + }; + static_assert(sizeof(CheatEntry) == 0x44C, "CheatEntry is an invalid size"); + static_assert(std::is_trivial_v, "CheatEntry type must be trivially copyable."); + + struct FrozenAddressValue { + u64 value; + u8 width; + }; + static_assert(sizeof(FrozenAddressValue) == 0x10, "FrozenAddressValue is an invalid size"); + + struct FrozenAddressEntry { + u64 address; + FrozenAddressValue value; + }; + static_assert(sizeof(FrozenAddressEntry) == 0x18, "FrozenAddressEntry is an invalid size"); +} // namespace Service::DMNT diff --git a/src/core/hle/service/services.cpp b/src/core/hle/service/services.cpp index 636f54ad49..14a19d376c 100644 --- a/src/core/hle/service/services.cpp +++ b/src/core/hle/service/services.cpp @@ -16,6 +16,7 @@ #include "core/hle/service/btdrv/btdrv.h" #include "core/hle/service/btm/btm.h" #include "core/hle/service/caps/caps.h" +#include "core/hle/service/dmnt/dmnt.h" #include "core/hle/service/erpt/erpt.h" #include "core/hle/service/es/es.h" #include "core/hle/service/eupld/eupld.h" @@ -107,6 +108,7 @@ Services::Services(std::shared_ptr& sm, Core::System& system {"btdrv", &BtDrv::LoopProcess}, {"btm", &BTM::LoopProcess}, {"capsrv", &Capture::LoopProcess}, + {"dmnt", &DMNT::LoopProcess}, {"erpt", &ERPT::LoopProcess}, {"es", &ES::LoopProcess}, {"eupld", &EUPLD::LoopProcess}, diff --git a/src/core/memory/cheat_engine.cpp b/src/core/memory/cheat_engine.cpp deleted file mode 100644 index 77cd587238..0000000000 --- a/src/core/memory/cheat_engine.cpp +++ /dev/null @@ -1,286 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project -// SPDX-License-Identifier: GPL-3.0-or-later - -// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#include -#include "common/hex_util.h" -#include "common/swap.h" -#include "core/arm/debug.h" -#include "core/core.h" -#include "core/core_timing.h" -#include "core/hle/kernel/k_page_table.h" -#include "core/hle/kernel/k_process.h" -#include "core/hle/kernel/k_process_page_table.h" -#include "core/hle/kernel/svc_types.h" -#include "core/hle/service/hid/hid_server.h" -#include "core/hle/service/sm/sm.h" -#include "core/memory.h" -#include "core/memory/cheat_engine.h" -#include "hid_core/resource_manager.h" -#include "hid_core/resources/npad/npad.h" - -namespace Core::Memory { -namespace { -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) { - ++end_index; - if (end_index > data.size()) { - return {}; - } - } - - out_name_size = end_index - start_index; - - // Clamp name if it's too big - if (out_name_size > sizeof(CheatDefinition::readable_name)) { - end_index = start_index + sizeof(CheatDefinition::readable_name); - } - - return data.substr(start_index, end_index - start_index); -} -} // Anonymous namespace - -StandardVmCallbacks::StandardVmCallbacks(System& system_, const CheatProcessMetadata& metadata_) - : metadata{metadata_}, system{system_} {} - -StandardVmCallbacks::~StandardVmCallbacks() = default; - -void StandardVmCallbacks::MemoryReadUnsafe(VAddr address, void* data, u64 size) { - // Return zero on invalid address - if (!IsAddressInRange(address) || !system.ApplicationMemory().IsValidVirtualAddress(address)) { - std::memset(data, 0, size); - return; - } - - system.ApplicationMemory().ReadBlock(address, data, size); -} - -void StandardVmCallbacks::MemoryWriteUnsafe(VAddr address, const void* data, u64 size) { - // Skip invalid memory write address - if (!IsAddressInRange(address) || !system.ApplicationMemory().IsValidVirtualAddress(address)) { - return; - } - - if (system.ApplicationMemory().WriteBlock(address, data, size)) { - Core::InvalidateInstructionCacheRange(system.ApplicationProcess(), address, size); - } -} - -u64 StandardVmCallbacks::HidKeysDown() { - const auto hid = system.ServiceManager().GetService("hid"); - if (hid == nullptr) { - LOG_WARNING(CheatEngine, "Attempted to read input state, but hid is not initialized!"); - return 0; - } - - const auto applet_resource = hid->GetResourceManager(); - if (applet_resource == nullptr || applet_resource->GetNpad() == nullptr) { - LOG_WARNING(CheatEngine, - "Attempted to read input state, but applet resource is not initialized!"); - return 0; - } - - const auto press_state = applet_resource->GetNpad()->GetAndResetPressState(); - return static_cast(press_state & HID::NpadButton::All); -} - -void StandardVmCallbacks::PauseProcess() { - if (system.ApplicationProcess()->IsSuspended()) { - return; - } - system.ApplicationProcess()->SetActivity(Kernel::Svc::ProcessActivity::Paused); -} - -void StandardVmCallbacks::ResumeProcess() { - if (!system.ApplicationProcess()->IsSuspended()) { - return; - } - system.ApplicationProcess()->SetActivity(Kernel::Svc::ProcessActivity::Runnable); -} - -void StandardVmCallbacks::DebugLog(u8 id, u64 value) { - LOG_INFO(CheatEngine, "Cheat triggered DebugLog: ID '{:01X}' Value '{:016X}'", id, value); -} - -void StandardVmCallbacks::CommandLog(std::string_view data) { - LOG_DEBUG(CheatEngine, "[DmntCheatVm]: {}", - data.back() == '\n' ? data.substr(0, data.size() - 1) : data); -} - -bool StandardVmCallbacks::IsAddressInRange(VAddr in) const { - if ((in < metadata.main_nso_extents.base || - in >= metadata.main_nso_extents.base + metadata.main_nso_extents.size) && - (in < metadata.heap_extents.base || - in >= metadata.heap_extents.base + metadata.heap_extents.size) && - (in < metadata.alias_extents.base || - in >= metadata.alias_extents.base + metadata.alias_extents.size) && - (in < metadata.aslr_extents.base || - in >= metadata.aslr_extents.base + metadata.aslr_extents.size)) { - LOG_DEBUG(CheatEngine, - "Cheat attempting to access memory at invalid address={:016X}, if this " - "persists, " - "the cheat may be incorrect. However, this may be normal early in execution if " - "the game has not properly set up yet.", - in); - return false; ///< Invalid addresses will hard crash - } - - return true; -} - -CheatParser::~CheatParser() = default; - -TextCheatParser::~TextCheatParser() = default; - -std::vector TextCheatParser::Parse(std::string_view data) const { - std::vector out(1); - std::optional current_entry; - - for (std::size_t i = 0; i < data.size(); ++i) { - if (::isspace(data[i])) { - continue; - } - - if (data[i] == '{') { - current_entry = 0; - - if (out[*current_entry].definition.num_opcodes > 0) { - return {}; - } - - std::size_t name_size{}; - const auto name = ExtractName(name_size, data, i + 1, '}'); - if (name.empty()) { - return {}; - } - - std::memcpy(out[*current_entry].definition.readable_name.data(), name.data(), - std::min(out[*current_entry].definition.readable_name.size(), - name.size())); - out[*current_entry] - .definition.readable_name[out[*current_entry].definition.readable_name.size() - 1] = - '\0'; - - i += name_size + 1; - } else if (data[i] == '[') { - current_entry = out.size(); - out.emplace_back(); - - std::size_t name_size{}; - const auto name = ExtractName(name_size, data, i + 1, ']'); - if (name.empty()) { - return {}; - } - - std::memcpy(out[*current_entry].definition.readable_name.data(), name.data(), - std::min(out[*current_entry].definition.readable_name.size(), - name.size())); - out[*current_entry] - .definition.readable_name[out[*current_entry].definition.readable_name.size() - 1] = - '\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()) { - return {}; - } - - const auto hex = std::string(data.substr(i, 8)); - if (!std::all_of(hex.begin(), hex.end(), ::isxdigit)) { - 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; - - i += 8; - } else { - return {}; - } - } - - out[0].enabled = out[0].definition.num_opcodes > 0; - out[0].cheat_id = 0; - - for (u32 i = 1; i < out.size(); ++i) { - out[i].enabled = out[i].definition.num_opcodes > 0; - out[i].cheat_id = i; - } - - return out; -} - -CheatEngine::CheatEngine(System& system_, std::vector cheats_, - const std::array& build_id_) - : vm{std::make_unique(system_, metadata)}, - cheats(std::move(cheats_)), core_timing{system_.CoreTiming()}, system{system_} { - metadata.main_nso_build_id = build_id_; -} - -CheatEngine::~CheatEngine() { - core_timing.UnscheduleEvent(event); -} - -void CheatEngine::Initialize() { - event = Core::Timing::CreateEvent( - "CheatEngine::FrameCallback::" + Common::HexToString(metadata.main_nso_build_id), - [this](s64 time, - std::chrono::nanoseconds ns_late) -> std::optional { - FrameCallback(ns_late); - return std::nullopt; - }); - core_timing.ScheduleLoopingEvent(CHEAT_ENGINE_NS, CHEAT_ENGINE_NS, event); - - metadata.process_id = system.ApplicationProcess()->GetProcessId(); - metadata.title_id = system.GetApplicationProcessProgramID(); - - const auto& page_table = system.ApplicationProcess()->GetPageTable(); - metadata.heap_extents = { - .base = GetInteger(page_table.GetHeapRegionStart()), - .size = page_table.GetHeapRegionSize(), - }; - metadata.aslr_extents = { - .base = GetInteger(page_table.GetAliasCodeRegionStart()), - .size = page_table.GetAliasCodeRegionSize(), - }; - metadata.alias_extents = { - .base = GetInteger(page_table.GetAliasRegionStart()), - .size = page_table.GetAliasRegionSize(), - }; - - is_pending_reload.exchange(true); -} - -void CheatEngine::SetMainMemoryParameters(VAddr main_region_begin, u64 main_region_size) { - metadata.main_nso_extents = { - .base = main_region_begin, - .size = main_region_size, - }; -} - -void CheatEngine::Reload(std::vector reload_cheats) { - cheats = std::move(reload_cheats); - is_pending_reload.exchange(true); -} - -void CheatEngine::FrameCallback(std::chrono::nanoseconds ns_late) { - if (is_pending_reload.exchange(false)) { - vm.LoadProgram(cheats); - } - - if (vm.GetProgramSize() == 0) { - return; - } - - vm.Execute(metadata); -} - -} // namespace Core::Memory diff --git a/src/core/memory/cheat_engine.h b/src/core/memory/cheat_engine.h deleted file mode 100644 index f52f2be7c3..0000000000 --- a/src/core/memory/cheat_engine.h +++ /dev/null @@ -1,88 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#pragma once - -#include -#include -#include -#include -#include "common/common_types.h" -#include "core/memory/dmnt_cheat_types.h" -#include "core/memory/dmnt_cheat_vm.h" - -namespace Core { -class System; -} - -namespace Core::Timing { -class CoreTiming; -struct EventType; -} // namespace Core::Timing - -namespace Core::Memory { - -class StandardVmCallbacks : public DmntCheatVm::Callbacks { -public: - StandardVmCallbacks(System& system_, const CheatProcessMetadata& metadata_); - ~StandardVmCallbacks() override; - - void MemoryReadUnsafe(VAddr address, void* data, u64 size) override; - void MemoryWriteUnsafe(VAddr address, const void* data, u64 size) override; - u64 HidKeysDown() override; - void PauseProcess() override; - void ResumeProcess() override; - void DebugLog(u8 id, u64 value) override; - void CommandLog(std::string_view data) override; - -private: - bool IsAddressInRange(VAddr address) const; - - const CheatProcessMetadata& metadata; - Core::System& system; -}; - -// Intermediary class that parses a text file or other disk format for storing cheats into a -// CheatList object, that can be used for execution. -class CheatParser { -public: - virtual ~CheatParser(); - - [[nodiscard]] virtual std::vector Parse(std::string_view data) const = 0; -}; - -// CheatParser implementation that parses text files -class TextCheatParser final : public CheatParser { -public: - ~TextCheatParser() override; - - [[nodiscard]] std::vector Parse(std::string_view data) const override; -}; - -// Class that encapsulates a CheatList and manages its interaction with memory and CoreTiming -class CheatEngine final { -public: - CheatEngine(System& system_, std::vector cheats_, - const std::array& build_id_); - ~CheatEngine(); - - void Initialize(); - void SetMainMemoryParameters(VAddr main_region_begin, u64 main_region_size); - - void Reload(std::vector reload_cheats); - -private: - void FrameCallback(std::chrono::nanoseconds ns_late); - - DmntCheatVm vm; - CheatProcessMetadata metadata; - - std::vector cheats; - std::atomic_bool is_pending_reload{false}; - - std::shared_ptr event; - Core::Timing::CoreTiming& core_timing; - Core::System& system; -}; - -} // namespace Core::Memory diff --git a/src/core/memory/dmnt_cheat_types.h b/src/core/memory/dmnt_cheat_types.h deleted file mode 100644 index 64c072d3de..0000000000 --- a/src/core/memory/dmnt_cheat_types.h +++ /dev/null @@ -1,37 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#pragma once - -#include "common/common_types.h" - -namespace Core::Memory { - -struct MemoryRegionExtents { - u64 base{}; - u64 size{}; -}; - -struct CheatProcessMetadata { - u64 process_id{}; - u64 title_id{}; - MemoryRegionExtents main_nso_extents{}; - MemoryRegionExtents heap_extents{}; - MemoryRegionExtents alias_extents{}; - MemoryRegionExtents aslr_extents{}; - std::array main_nso_build_id{}; -}; - -struct CheatDefinition { - std::array readable_name{}; - u32 num_opcodes{}; - std::array opcodes{}; -}; - -struct CheatEntry { - bool enabled{}; - u32 cheat_id{}; - CheatDefinition definition{}; -}; - -} // namespace Core::Memory diff --git a/src/core/memory/dmnt_cheat_vm.cpp b/src/core/memory/dmnt_cheat_vm.cpp deleted file mode 100644 index caceeec4fc..0000000000 --- a/src/core/memory/dmnt_cheat_vm.cpp +++ /dev/null @@ -1,1268 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#include "common/assert.h" -#include "common/scope_exit.h" -#include "core/memory/dmnt_cheat_types.h" -#include "core/memory/dmnt_cheat_vm.h" - -namespace Core::Memory { - -DmntCheatVm::DmntCheatVm(std::unique_ptr callbacks_) - : callbacks(std::move(callbacks_)) {} - -DmntCheatVm::~DmntCheatVm() = default; - -void DmntCheatVm::DebugLog(u32 log_id, u64 value) { - callbacks->DebugLog(static_cast(log_id), value); -} - -void DmntCheatVm::LogOpcode(const CheatVmOpcode& opcode) { - if (auto store_static = std::get_if(&opcode.opcode)) { - callbacks->CommandLog("Opcode: Store Static"); - callbacks->CommandLog(fmt::format("Bit Width: {:X}", store_static->bit_width)); - callbacks->CommandLog( - fmt::format("Mem Type: {:X}", static_cast(store_static->mem_type))); - callbacks->CommandLog(fmt::format("Reg Idx: {:X}", store_static->offset_register)); - callbacks->CommandLog(fmt::format("Rel Addr: {:X}", store_static->rel_address)); - callbacks->CommandLog(fmt::format("Value: {:X}", store_static->value.bit64)); - } else if (auto begin_cond = std::get_if(&opcode.opcode)) { - callbacks->CommandLog("Opcode: Begin Conditional"); - callbacks->CommandLog(fmt::format("Bit Width: {:X}", begin_cond->bit_width)); - callbacks->CommandLog( - fmt::format("Mem Type: {:X}", static_cast(begin_cond->mem_type))); - callbacks->CommandLog( - fmt::format("Cond Type: {:X}", static_cast(begin_cond->cond_type))); - callbacks->CommandLog(fmt::format("Rel Addr: {:X}", begin_cond->rel_address)); - callbacks->CommandLog(fmt::format("Value: {:X}", begin_cond->value.bit64)); - } else if (std::holds_alternative(opcode.opcode)) { - callbacks->CommandLog("Opcode: End Conditional"); - } else if (auto ctrl_loop = std::get_if(&opcode.opcode)) { - if (ctrl_loop->start_loop) { - callbacks->CommandLog("Opcode: Start Loop"); - callbacks->CommandLog(fmt::format("Reg Idx: {:X}", ctrl_loop->reg_index)); - callbacks->CommandLog(fmt::format("Num Iters: {:X}", ctrl_loop->num_iters)); - } else { - callbacks->CommandLog("Opcode: End Loop"); - callbacks->CommandLog(fmt::format("Reg Idx: {:X}", ctrl_loop->reg_index)); - } - } else if (auto ldr_static = std::get_if(&opcode.opcode)) { - callbacks->CommandLog("Opcode: Load Register Static"); - callbacks->CommandLog(fmt::format("Reg Idx: {:X}", ldr_static->reg_index)); - callbacks->CommandLog(fmt::format("Value: {:X}", ldr_static->value)); - } else if (auto ldr_memory = std::get_if(&opcode.opcode)) { - callbacks->CommandLog("Opcode: Load Register Memory"); - callbacks->CommandLog(fmt::format("Bit Width: {:X}", ldr_memory->bit_width)); - callbacks->CommandLog(fmt::format("Reg Idx: {:X}", ldr_memory->reg_index)); - callbacks->CommandLog( - fmt::format("Mem Type: {:X}", static_cast(ldr_memory->mem_type))); - callbacks->CommandLog(fmt::format("From Reg: {:d}", ldr_memory->load_from_reg)); - callbacks->CommandLog(fmt::format("Rel Addr: {:X}", ldr_memory->rel_address)); - } else if (auto str_static = std::get_if(&opcode.opcode)) { - callbacks->CommandLog("Opcode: Store Static to Address"); - callbacks->CommandLog(fmt::format("Bit Width: {:X}", str_static->bit_width)); - callbacks->CommandLog(fmt::format("Reg Idx: {:X}", str_static->reg_index)); - if (str_static->add_offset_reg) { - callbacks->CommandLog(fmt::format("O Reg Idx: {:X}", str_static->offset_reg_index)); - } - callbacks->CommandLog(fmt::format("Incr Reg: {:d}", str_static->increment_reg)); - callbacks->CommandLog(fmt::format("Value: {:X}", str_static->value)); - } else if (auto perform_math_static = - std::get_if(&opcode.opcode)) { - callbacks->CommandLog("Opcode: Perform Static Arithmetic"); - callbacks->CommandLog(fmt::format("Bit Width: {:X}", perform_math_static->bit_width)); - callbacks->CommandLog(fmt::format("Reg Idx: {:X}", perform_math_static->reg_index)); - callbacks->CommandLog( - fmt::format("Math Type: {:X}", static_cast(perform_math_static->math_type))); - callbacks->CommandLog(fmt::format("Value: {:X}", perform_math_static->value)); - } else if (auto begin_keypress_cond = - std::get_if(&opcode.opcode)) { - callbacks->CommandLog("Opcode: Begin Keypress Conditional"); - callbacks->CommandLog(fmt::format("Key Mask: {:X}", begin_keypress_cond->key_mask)); - } else if (auto perform_math_reg = - std::get_if(&opcode.opcode)) { - callbacks->CommandLog("Opcode: Perform Register Arithmetic"); - callbacks->CommandLog(fmt::format("Bit Width: {:X}", perform_math_reg->bit_width)); - callbacks->CommandLog(fmt::format("Dst Idx: {:X}", perform_math_reg->dst_reg_index)); - callbacks->CommandLog(fmt::format("Src1 Idx: {:X}", perform_math_reg->src_reg_1_index)); - if (perform_math_reg->has_immediate) { - callbacks->CommandLog(fmt::format("Value: {:X}", perform_math_reg->value.bit64)); - } else { - callbacks->CommandLog( - fmt::format("Src2 Idx: {:X}", perform_math_reg->src_reg_2_index)); - } - } else if (auto str_register = std::get_if(&opcode.opcode)) { - callbacks->CommandLog("Opcode: Store Register to Address"); - callbacks->CommandLog(fmt::format("Bit Width: {:X}", str_register->bit_width)); - callbacks->CommandLog(fmt::format("S Reg Idx: {:X}", str_register->str_reg_index)); - callbacks->CommandLog(fmt::format("A Reg Idx: {:X}", str_register->addr_reg_index)); - callbacks->CommandLog(fmt::format("Incr Reg: {:d}", str_register->increment_reg)); - switch (str_register->ofs_type) { - case StoreRegisterOffsetType::None: - break; - case StoreRegisterOffsetType::Reg: - callbacks->CommandLog(fmt::format("O Reg Idx: {:X}", str_register->ofs_reg_index)); - break; - case StoreRegisterOffsetType::Imm: - callbacks->CommandLog(fmt::format("Rel Addr: {:X}", str_register->rel_address)); - break; - case StoreRegisterOffsetType::MemReg: - callbacks->CommandLog( - fmt::format("Mem Type: {:X}", static_cast(str_register->mem_type))); - break; - case StoreRegisterOffsetType::MemImm: - case StoreRegisterOffsetType::MemImmReg: - callbacks->CommandLog( - fmt::format("Mem Type: {:X}", static_cast(str_register->mem_type))); - callbacks->CommandLog(fmt::format("Rel Addr: {:X}", str_register->rel_address)); - break; - } - } else if (auto begin_reg_cond = std::get_if(&opcode.opcode)) { - callbacks->CommandLog("Opcode: Begin Register Conditional"); - callbacks->CommandLog(fmt::format("Bit Width: {:X}", begin_reg_cond->bit_width)); - callbacks->CommandLog( - fmt::format("Cond Type: {:X}", static_cast(begin_reg_cond->cond_type))); - callbacks->CommandLog(fmt::format("V Reg Idx: {:X}", begin_reg_cond->val_reg_index)); - switch (begin_reg_cond->comp_type) { - case CompareRegisterValueType::StaticValue: - callbacks->CommandLog("Comp Type: Static Value"); - callbacks->CommandLog(fmt::format("Value: {:X}", begin_reg_cond->value.bit64)); - break; - case CompareRegisterValueType::OtherRegister: - callbacks->CommandLog("Comp Type: Other Register"); - callbacks->CommandLog(fmt::format("X Reg Idx: {:X}", begin_reg_cond->other_reg_index)); - break; - case CompareRegisterValueType::MemoryRelAddr: - callbacks->CommandLog("Comp Type: Memory Relative Address"); - callbacks->CommandLog( - fmt::format("Mem Type: {:X}", static_cast(begin_reg_cond->mem_type))); - callbacks->CommandLog(fmt::format("Rel Addr: {:X}", begin_reg_cond->rel_address)); - break; - case CompareRegisterValueType::MemoryOfsReg: - callbacks->CommandLog("Comp Type: Memory Offset Register"); - callbacks->CommandLog( - fmt::format("Mem Type: {:X}", static_cast(begin_reg_cond->mem_type))); - callbacks->CommandLog(fmt::format("O Reg Idx: {:X}", begin_reg_cond->ofs_reg_index)); - break; - case CompareRegisterValueType::RegisterRelAddr: - callbacks->CommandLog("Comp Type: Register Relative Address"); - callbacks->CommandLog(fmt::format("A Reg Idx: {:X}", begin_reg_cond->addr_reg_index)); - callbacks->CommandLog(fmt::format("Rel Addr: {:X}", begin_reg_cond->rel_address)); - break; - case CompareRegisterValueType::RegisterOfsReg: - callbacks->CommandLog("Comp Type: Register Offset Register"); - callbacks->CommandLog(fmt::format("A Reg Idx: {:X}", begin_reg_cond->addr_reg_index)); - callbacks->CommandLog(fmt::format("O Reg Idx: {:X}", begin_reg_cond->ofs_reg_index)); - break; - } - } else if (auto save_restore_reg = std::get_if(&opcode.opcode)) { - callbacks->CommandLog("Opcode: Save or Restore Register"); - callbacks->CommandLog(fmt::format("Dst Idx: {:X}", save_restore_reg->dst_index)); - callbacks->CommandLog(fmt::format("Src Idx: {:X}", save_restore_reg->src_index)); - callbacks->CommandLog( - fmt::format("Op Type: {:d}", static_cast(save_restore_reg->op_type))); - } else if (auto save_restore_regmask = - std::get_if(&opcode.opcode)) { - callbacks->CommandLog("Opcode: Save or Restore Register Mask"); - callbacks->CommandLog( - fmt::format("Op Type: {:d}", static_cast(save_restore_regmask->op_type))); - for (std::size_t i = 0; i < NumRegisters; i++) { - callbacks->CommandLog( - fmt::format("Act[{:02X}]: {:d}", i, save_restore_regmask->should_operate[i])); - } - } else if (auto rw_static_reg = std::get_if(&opcode.opcode)) { - callbacks->CommandLog("Opcode: Read/Write Static Register"); - if (rw_static_reg->static_idx < NumReadableStaticRegisters) { - callbacks->CommandLog("Op Type: ReadStaticRegister"); - } else { - callbacks->CommandLog("Op Type: WriteStaticRegister"); - } - callbacks->CommandLog(fmt::format("Reg Idx {:X}", rw_static_reg->idx)); - callbacks->CommandLog(fmt::format("Stc Idx {:X}", rw_static_reg->static_idx)); - } else if (auto debug_log = std::get_if(&opcode.opcode)) { - callbacks->CommandLog("Opcode: Debug Log"); - callbacks->CommandLog(fmt::format("Bit Width: {:X}", debug_log->bit_width)); - callbacks->CommandLog(fmt::format("Log ID: {:X}", debug_log->log_id)); - callbacks->CommandLog( - fmt::format("Val Type: {:X}", static_cast(debug_log->val_type))); - switch (debug_log->val_type) { - case DebugLogValueType::RegisterValue: - callbacks->CommandLog("Val Type: Register Value"); - callbacks->CommandLog(fmt::format("X Reg Idx: {:X}", debug_log->val_reg_index)); - break; - case DebugLogValueType::MemoryRelAddr: - callbacks->CommandLog("Val Type: Memory Relative Address"); - callbacks->CommandLog( - fmt::format("Mem Type: {:X}", static_cast(debug_log->mem_type))); - callbacks->CommandLog(fmt::format("Rel Addr: {:X}", debug_log->rel_address)); - break; - case DebugLogValueType::MemoryOfsReg: - callbacks->CommandLog("Val Type: Memory Offset Register"); - callbacks->CommandLog( - fmt::format("Mem Type: {:X}", static_cast(debug_log->mem_type))); - callbacks->CommandLog(fmt::format("O Reg Idx: {:X}", debug_log->ofs_reg_index)); - break; - case DebugLogValueType::RegisterRelAddr: - callbacks->CommandLog("Val Type: Register Relative Address"); - callbacks->CommandLog(fmt::format("A Reg Idx: {:X}", debug_log->addr_reg_index)); - callbacks->CommandLog(fmt::format("Rel Addr: {:X}", debug_log->rel_address)); - break; - case DebugLogValueType::RegisterOfsReg: - callbacks->CommandLog("Val Type: Register Offset Register"); - callbacks->CommandLog(fmt::format("A Reg Idx: {:X}", debug_log->addr_reg_index)); - callbacks->CommandLog(fmt::format("O Reg Idx: {:X}", debug_log->ofs_reg_index)); - break; - } - } else if (auto instr = std::get_if(&opcode.opcode)) { - callbacks->CommandLog(fmt::format("Unknown opcode: {:X}", static_cast(instr->opcode))); - } -} - -DmntCheatVm::Callbacks::~Callbacks() = default; - -bool DmntCheatVm::DecodeNextOpcode(CheatVmOpcode& out) { - // If we've ever seen a decode failure, return false. - bool valid = decode_success; - CheatVmOpcode opcode = {}; - SCOPE_EXIT { - decode_success &= valid; - if (valid) { - out = opcode; - } - }; - - // Helper function for getting instruction dwords. - const auto GetNextDword = [&] { - if (instruction_ptr >= num_opcodes) { - valid = false; - return static_cast(0); - } - return program[instruction_ptr++]; - }; - - // Helper function for parsing a VmInt. - const auto GetNextVmInt = [&](const u32 bit_width) { - VmInt val{}; - - const u32 first_dword = GetNextDword(); - switch (bit_width) { - case 1: - val.bit8 = static_cast(first_dword); - break; - case 2: - val.bit16 = static_cast(first_dword); - break; - case 4: - val.bit32 = first_dword; - break; - case 8: - val.bit64 = (static_cast(first_dword) << 32ul) | static_cast(GetNextDword()); - break; - } - - return val; - }; - - // Read opcode. - const u32 first_dword = GetNextDword(); - if (!valid) { - return valid; - } - - auto opcode_type = static_cast(((first_dword >> 28) & 0xF)); - if (opcode_type >= CheatVmOpcodeType::ExtendedWidth) { - opcode_type = static_cast((static_cast(opcode_type) << 4) | - ((first_dword >> 24) & 0xF)); - } - if (opcode_type >= CheatVmOpcodeType::DoubleExtendedWidth) { - opcode_type = static_cast((static_cast(opcode_type) << 4) | - ((first_dword >> 20) & 0xF)); - } - - // detect condition start. - switch (opcode_type) { - case CheatVmOpcodeType::BeginConditionalBlock: - case CheatVmOpcodeType::BeginKeypressConditionalBlock: - case CheatVmOpcodeType::BeginRegisterConditionalBlock: - opcode.begin_conditional_block = true; - break; - default: - opcode.begin_conditional_block = false; - break; - } - - switch (opcode_type) { - case CheatVmOpcodeType::StoreStatic: { - // 0TMR00AA AAAAAAAA YYYYYYYY (YYYYYYYY) - // Read additional words. - const u32 second_dword = GetNextDword(); - const u32 bit_width = (first_dword >> 24) & 0xF; - - opcode.opcode = StoreStaticOpcode{ - .bit_width = bit_width, - .mem_type = static_cast((first_dword >> 20) & 0xF), - .offset_register = (first_dword >> 16) & 0xF, - .rel_address = (static_cast(first_dword & 0xFF) << 32) | second_dword, - .value = GetNextVmInt(bit_width), - }; - } break; - case CheatVmOpcodeType::BeginConditionalBlock: { - // 1TMC00AA AAAAAAAA YYYYYYYY (YYYYYYYY) - // Read additional words. - const u32 second_dword = GetNextDword(); - const u32 bit_width = (first_dword >> 24) & 0xF; - - opcode.opcode = BeginConditionalOpcode{ - .bit_width = bit_width, - .mem_type = static_cast((first_dword >> 20) & 0xF), - .cond_type = static_cast((first_dword >> 16) & 0xF), - .rel_address = (static_cast(first_dword & 0xFF) << 32) | second_dword, - .value = GetNextVmInt(bit_width), - }; - } break; - case CheatVmOpcodeType::EndConditionalBlock: { - // 20000000 - opcode.opcode = EndConditionalOpcode{ - .is_else = ((first_dword >> 24) & 0xf) == 1, - }; - } break; - case CheatVmOpcodeType::ControlLoop: { - // 300R0000 VVVVVVVV - // 310R0000 - // Parse register, whether loop start or loop end. - ControlLoopOpcode ctrl_loop{ - .start_loop = ((first_dword >> 24) & 0xF) == 0, - .reg_index = (first_dword >> 20) & 0xF, - .num_iters = 0, - }; - - // Read number of iters if loop start. - if (ctrl_loop.start_loop) { - ctrl_loop.num_iters = GetNextDword(); - } - opcode.opcode = ctrl_loop; - } break; - case CheatVmOpcodeType::LoadRegisterStatic: { - // 400R0000 VVVVVVVV VVVVVVVV - // Read additional words. - opcode.opcode = LoadRegisterStaticOpcode{ - .reg_index = (first_dword >> 16) & 0xF, - .value = (static_cast(GetNextDword()) << 32) | GetNextDword(), - }; - } break; - case CheatVmOpcodeType::LoadRegisterMemory: { - // 5TMRI0AA AAAAAAAA - // Read additional words. - const u32 second_dword = GetNextDword(); - opcode.opcode = LoadRegisterMemoryOpcode{ - .bit_width = (first_dword >> 24) & 0xF, - .mem_type = static_cast((first_dword >> 20) & 0xF), - .reg_index = ((first_dword >> 16) & 0xF), - .load_from_reg = ((first_dword >> 12) & 0xF) != 0, - .rel_address = (static_cast(first_dword & 0xFF) << 32) | second_dword, - }; - } break; - case CheatVmOpcodeType::StoreStaticToAddress: { - // 6T0RIor0 VVVVVVVV VVVVVVVV - // Read additional words. - opcode.opcode = StoreStaticToAddressOpcode{ - .bit_width = (first_dword >> 24) & 0xF, - .reg_index = (first_dword >> 16) & 0xF, - .increment_reg = ((first_dword >> 12) & 0xF) != 0, - .add_offset_reg = ((first_dword >> 8) & 0xF) != 0, - .offset_reg_index = (first_dword >> 4) & 0xF, - .value = (static_cast(GetNextDword()) << 32) | GetNextDword(), - }; - } break; - case CheatVmOpcodeType::PerformArithmeticStatic: { - // 7T0RC000 VVVVVVVV - // Read additional words. - opcode.opcode = PerformArithmeticStaticOpcode{ - .bit_width = (first_dword >> 24) & 0xF, - .reg_index = ((first_dword >> 16) & 0xF), - .math_type = static_cast((first_dword >> 12) & 0xF), - .value = GetNextDword(), - }; - } break; - case CheatVmOpcodeType::BeginKeypressConditionalBlock: { - // 8kkkkkkk - // Just parse the mask. - opcode.opcode = BeginKeypressConditionalOpcode{ - .key_mask = first_dword & 0x0FFFFFFF, - }; - } break; - case CheatVmOpcodeType::PerformArithmeticRegister: { - // 9TCRSIs0 (VVVVVVVV (VVVVVVVV)) - PerformArithmeticRegisterOpcode perform_math_reg{ - .bit_width = (first_dword >> 24) & 0xF, - .math_type = static_cast((first_dword >> 20) & 0xF), - .dst_reg_index = (first_dword >> 16) & 0xF, - .src_reg_1_index = (first_dword >> 12) & 0xF, - .src_reg_2_index = 0, - .has_immediate = ((first_dword >> 8) & 0xF) != 0, - .value = {}, - }; - if (perform_math_reg.has_immediate) { - perform_math_reg.src_reg_2_index = 0; - perform_math_reg.value = GetNextVmInt(perform_math_reg.bit_width); - } else { - perform_math_reg.src_reg_2_index = ((first_dword >> 4) & 0xF); - } - opcode.opcode = perform_math_reg; - } break; - case CheatVmOpcodeType::StoreRegisterToAddress: { - // ATSRIOxa (aaaaaaaa) - // A = opcode 10 - // T = bit width - // S = src register index - // R = address register index - // I = 1 if increment address register, 0 if not increment address register - // O = offset type, 0 = None, 1 = Register, 2 = Immediate, 3 = Memory Region, - // 4 = Memory Region + Relative Address (ignore address register), 5 = Memory Region + - // Relative Address - // x = offset register (for offset type 1), memory type (for offset type 3) - // a = relative address (for offset type 2+3) - StoreRegisterToAddressOpcode str_register{ - .bit_width = (first_dword >> 24) & 0xF, - .str_reg_index = (first_dword >> 20) & 0xF, - .addr_reg_index = (first_dword >> 16) & 0xF, - .increment_reg = ((first_dword >> 12) & 0xF) != 0, - .ofs_type = static_cast(((first_dword >> 8) & 0xF)), - .mem_type = MemoryAccessType::MainNso, - .ofs_reg_index = (first_dword >> 4) & 0xF, - .rel_address = 0, - }; - switch (str_register.ofs_type) { - case StoreRegisterOffsetType::None: - case StoreRegisterOffsetType::Reg: - // Nothing more to do - break; - case StoreRegisterOffsetType::Imm: - str_register.rel_address = (static_cast(first_dword & 0xF) << 32) | GetNextDword(); - break; - case StoreRegisterOffsetType::MemReg: - str_register.mem_type = static_cast((first_dword >> 4) & 0xF); - break; - case StoreRegisterOffsetType::MemImm: - case StoreRegisterOffsetType::MemImmReg: - str_register.mem_type = static_cast((first_dword >> 4) & 0xF); - str_register.rel_address = (static_cast(first_dword & 0xF) << 32) | GetNextDword(); - break; - default: - str_register.ofs_type = StoreRegisterOffsetType::None; - break; - } - opcode.opcode = str_register; - } break; - case CheatVmOpcodeType::BeginRegisterConditionalBlock: { - // C0TcSX## - // C0TcS0Ma aaaaaaaa - // C0TcS1Mr - // C0TcS2Ra aaaaaaaa - // C0TcS3Rr - // C0TcS400 VVVVVVVV (VVVVVVVV) - // C0TcS5X0 - // C0 = opcode 0xC0 - // T = bit width - // c = condition type. - // S = source register. - // X = value operand type, 0 = main/heap with relative offset, 1 = main/heap with offset - // register, - // 2 = register with relative offset, 3 = register with offset register, 4 = static - // value, 5 = other register. - // M = memory type. - // R = address register. - // a = relative address. - // r = offset register. - // X = other register. - // V = value. - - BeginRegisterConditionalOpcode begin_reg_cond{ - .bit_width = (first_dword >> 20) & 0xF, - .cond_type = static_cast((first_dword >> 16) & 0xF), - .val_reg_index = (first_dword >> 12) & 0xF, - .comp_type = static_cast((first_dword >> 8) & 0xF), - .mem_type = MemoryAccessType::MainNso, - .addr_reg_index = 0, - .other_reg_index = 0, - .ofs_reg_index = 0, - .rel_address = 0, - .value = {}, - }; - - switch (begin_reg_cond.comp_type) { - case CompareRegisterValueType::StaticValue: - begin_reg_cond.value = GetNextVmInt(begin_reg_cond.bit_width); - break; - case CompareRegisterValueType::OtherRegister: - begin_reg_cond.other_reg_index = ((first_dword >> 4) & 0xF); - break; - case CompareRegisterValueType::MemoryRelAddr: - begin_reg_cond.mem_type = static_cast((first_dword >> 4) & 0xF); - begin_reg_cond.rel_address = - (static_cast(first_dword & 0xF) << 32) | GetNextDword(); - break; - case CompareRegisterValueType::MemoryOfsReg: - begin_reg_cond.mem_type = static_cast((first_dword >> 4) & 0xF); - begin_reg_cond.ofs_reg_index = (first_dword & 0xF); - break; - case CompareRegisterValueType::RegisterRelAddr: - begin_reg_cond.addr_reg_index = (first_dword >> 4) & 0xF; - begin_reg_cond.rel_address = - (static_cast(first_dword & 0xF) << 32) | GetNextDword(); - break; - case CompareRegisterValueType::RegisterOfsReg: - begin_reg_cond.addr_reg_index = (first_dword >> 4) & 0xF; - begin_reg_cond.ofs_reg_index = first_dword & 0xF; - break; - } - opcode.opcode = begin_reg_cond; - } break; - case CheatVmOpcodeType::SaveRestoreRegister: { - // C10D0Sx0 - // C1 = opcode 0xC1 - // D = destination index. - // S = source index. - // x = 3 if clearing reg, 2 if clearing saved value, 1 if saving a register, 0 if restoring - // a register. - // NOTE: If we add more save slots later, current encoding is backwards compatible. - opcode.opcode = SaveRestoreRegisterOpcode{ - .dst_index = (first_dword >> 16) & 0xF, - .src_index = (first_dword >> 8) & 0xF, - .op_type = static_cast((first_dword >> 4) & 0xF), - }; - } break; - case CheatVmOpcodeType::SaveRestoreRegisterMask: { - // C2x0XXXX - // C2 = opcode 0xC2 - // x = 3 if clearing reg, 2 if clearing saved value, 1 if saving, 0 if restoring. - // X = 16-bit bitmask, bit i --> save or restore register i. - SaveRestoreRegisterMaskOpcode save_restore_regmask{ - .op_type = static_cast((first_dword >> 20) & 0xF), - .should_operate = {}, - }; - for (std::size_t i = 0; i < NumRegisters; i++) { - save_restore_regmask.should_operate[i] = (first_dword & (1U << i)) != 0; - } - opcode.opcode = save_restore_regmask; - } break; - case CheatVmOpcodeType::ReadWriteStaticRegister: { - // C3000XXx - // C3 = opcode 0xC3. - // XX = static register index. - // x = register index. - opcode.opcode = ReadWriteStaticRegisterOpcode{ - .static_idx = (first_dword >> 4) & 0xFF, - .idx = first_dword & 0xF, - }; - } break; - case CheatVmOpcodeType::PauseProcess: { - /* FF0????? */ - /* FF0 = opcode 0xFF0 */ - /* Pauses the current process. */ - opcode.opcode = PauseProcessOpcode{}; - } break; - case CheatVmOpcodeType::ResumeProcess: { - /* FF0????? */ - /* FF0 = opcode 0xFF0 */ - /* Pauses the current process. */ - opcode.opcode = ResumeProcessOpcode{}; - } break; - case CheatVmOpcodeType::DebugLog: { - // FFFTIX## - // FFFTI0Ma aaaaaaaa - // FFFTI1Mr - // FFFTI2Ra aaaaaaaa - // FFFTI3Rr - // FFFTI4X0 - // FFF = opcode 0xFFF - // T = bit width. - // I = log id. - // X = value operand type, 0 = main/heap with relative offset, 1 = main/heap with offset - // register, - // 2 = register with relative offset, 3 = register with offset register, 4 = register - // value. - // M = memory type. - // R = address register. - // a = relative address. - // r = offset register. - // X = value register. - DebugLogOpcode debug_log{ - .bit_width = (first_dword >> 16) & 0xF, - .log_id = (first_dword >> 12) & 0xF, - .val_type = static_cast((first_dword >> 8) & 0xF), - .mem_type = MemoryAccessType::MainNso, - .addr_reg_index = 0, - .val_reg_index = 0, - .ofs_reg_index = 0, - .rel_address = 0, - }; - - switch (debug_log.val_type) { - case DebugLogValueType::RegisterValue: - debug_log.val_reg_index = (first_dword >> 4) & 0xF; - break; - case DebugLogValueType::MemoryRelAddr: - debug_log.mem_type = static_cast((first_dword >> 4) & 0xF); - debug_log.rel_address = (static_cast(first_dword & 0xF) << 32) | GetNextDword(); - break; - case DebugLogValueType::MemoryOfsReg: - debug_log.mem_type = static_cast((first_dword >> 4) & 0xF); - debug_log.ofs_reg_index = first_dword & 0xF; - break; - case DebugLogValueType::RegisterRelAddr: - debug_log.addr_reg_index = (first_dword >> 4) & 0xF; - debug_log.rel_address = (static_cast(first_dword & 0xF) << 32) | GetNextDword(); - break; - case DebugLogValueType::RegisterOfsReg: - debug_log.addr_reg_index = (first_dword >> 4) & 0xF; - debug_log.ofs_reg_index = first_dword & 0xF; - break; - } - opcode.opcode = debug_log; - } break; - case CheatVmOpcodeType::ExtendedWidth: - case CheatVmOpcodeType::DoubleExtendedWidth: - default: - // Unrecognized instruction cannot be decoded. - valid = false; - opcode.opcode = UnrecognizedInstruction{opcode_type}; - break; - } - - // End decoding. - return valid; -} - -void DmntCheatVm::SkipConditionalBlock(bool is_if) { - if (condition_depth > 0) { - // We want to continue until we're out of the current block. - const std::size_t desired_depth = condition_depth - 1; - - CheatVmOpcode skip_opcode{}; - while (condition_depth > desired_depth && DecodeNextOpcode(skip_opcode)) { - // Decode instructions until we see end of the current conditional block. - // NOTE: This is broken in gateway's implementation. - // Gateway currently checks for "0x2" instead of "0x20000000" - // In addition, they do a linear scan instead of correctly decoding opcodes. - // This causes issues if "0x2" appears as an immediate in the conditional block... - - // We also support nesting of conditional blocks, and Gateway does not. - if (skip_opcode.begin_conditional_block) { - condition_depth++; - } else if (auto end_cond = std::get_if(&skip_opcode.opcode)) { - if (!end_cond->is_else) { - condition_depth--; - } else if (is_if && condition_depth - 1 == desired_depth) { - break; - } - } - } - } else { - // Skipping, but condition_depth = 0. - // This is an error condition. - // However, I don't actually believe it is possible for this to happen. - // I guess we'll throw a fatal error here, so as to encourage me to fix the VM - // in the event that someone triggers it? I don't know how you'd do that. - UNREACHABLE_MSG("Invalid condition depth in DMNT Cheat VM"); - } -} - -u64 DmntCheatVm::GetVmInt(VmInt value, u32 bit_width) { - switch (bit_width) { - case 1: - return value.bit8; - case 2: - return value.bit16; - case 4: - return value.bit32; - case 8: - return value.bit64; - default: - // Invalid bit width -> return 0. - return 0; - } -} - -u64 DmntCheatVm::GetCheatProcessAddress(const CheatProcessMetadata& metadata, - MemoryAccessType mem_type, u64 rel_address) { - switch (mem_type) { - case MemoryAccessType::MainNso: - default: - return metadata.main_nso_extents.base + rel_address; - case MemoryAccessType::Heap: - return metadata.heap_extents.base + rel_address; - case MemoryAccessType::Alias: - return metadata.alias_extents.base + rel_address; - case MemoryAccessType::Aslr: - return metadata.aslr_extents.base + rel_address; - } -} - -void DmntCheatVm::ResetState() { - registers.fill(0); - saved_values.fill(0); - loop_tops.fill(0); - instruction_ptr = 0; - condition_depth = 0; - decode_success = true; -} - -bool DmntCheatVm::LoadProgram(const std::vector& entries) { - // Reset opcode count. - num_opcodes = 0; - - 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; - } - - for (std::size_t n = 0; n < entries[i].definition.num_opcodes; n++) { - program[num_opcodes++] = entries[i].definition.opcodes[n]; - } - } - } - - return true; -} - -void DmntCheatVm::Execute(const CheatProcessMetadata& metadata) { - CheatVmOpcode cur_opcode{}; - - // 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))); - - // Clear VM state. - ResetState(); - - // Loop until program finishes. - while (DecodeNextOpcode(cur_opcode)) { - 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("SavedRegs[{:02X}]: {:016X}", i, saved_values[i])); - } - LogOpcode(cur_opcode); - - // Increment conditional depth, if relevant. - if (cur_opcode.begin_conditional_block) { - condition_depth++; - } - - if (auto store_static = std::get_if(&cur_opcode.opcode)) { - // Calculate address, write value to memory. - u64 dst_address = GetCheatProcessAddress(metadata, store_static->mem_type, - store_static->rel_address + - registers[store_static->offset_register]); - u64 dst_value = GetVmInt(store_static->value, store_static->bit_width); - switch (store_static->bit_width) { - case 1: - case 2: - case 4: - case 8: - callbacks->MemoryWriteUnsafe(dst_address, &dst_value, store_static->bit_width); - break; - } - } else if (auto begin_cond = std::get_if(&cur_opcode.opcode)) { - // Read value from memory. - u64 src_address = - GetCheatProcessAddress(metadata, begin_cond->mem_type, begin_cond->rel_address); - u64 src_value = 0; - switch (begin_cond->bit_width) { - case 1: - case 2: - case 4: - case 8: - callbacks->MemoryReadUnsafe(src_address, &src_value, begin_cond->bit_width); - break; - } - // Check against condition. - u64 cond_value = GetVmInt(begin_cond->value, begin_cond->bit_width); - bool cond_met = false; - switch (begin_cond->cond_type) { - case ConditionalComparisonType::GT: - cond_met = src_value > cond_value; - break; - case ConditionalComparisonType::GE: - cond_met = src_value >= cond_value; - break; - case ConditionalComparisonType::LT: - cond_met = src_value < cond_value; - break; - case ConditionalComparisonType::LE: - cond_met = src_value <= cond_value; - break; - case ConditionalComparisonType::EQ: - cond_met = src_value == cond_value; - break; - case ConditionalComparisonType::NE: - cond_met = src_value != cond_value; - break; - } - // Skip conditional block if condition not met. - if (!cond_met) { - SkipConditionalBlock(true); - } - } else if (auto end_cond = std::get_if(&cur_opcode.opcode)) { - if (end_cond->is_else) { - /* Skip to the end of the conditional block. */ - this->SkipConditionalBlock(false); - } else { - /* Decrement the condition depth. */ - /* We will assume, graciously, that mismatched conditional block ends are a nop. */ - if (condition_depth > 0) { - condition_depth--; - } - } - } else if (auto ctrl_loop = std::get_if(&cur_opcode.opcode)) { - if (ctrl_loop->start_loop) { - // Start a loop. - registers[ctrl_loop->reg_index] = ctrl_loop->num_iters; - loop_tops[ctrl_loop->reg_index] = instruction_ptr; - } else { - // End a loop. - registers[ctrl_loop->reg_index]--; - if (registers[ctrl_loop->reg_index] != 0) { - instruction_ptr = loop_tops[ctrl_loop->reg_index]; - } - } - } else if (auto ldr_static = std::get_if(&cur_opcode.opcode)) { - // Set a register to a static value. - registers[ldr_static->reg_index] = ldr_static->value; - } else if (auto ldr_memory = std::get_if(&cur_opcode.opcode)) { - // Choose source address. - u64 src_address; - if (ldr_memory->load_from_reg) { - src_address = registers[ldr_memory->reg_index] + ldr_memory->rel_address; - } else { - src_address = - GetCheatProcessAddress(metadata, ldr_memory->mem_type, ldr_memory->rel_address); - } - // Read into register. Gateway only reads on valid bitwidth. - switch (ldr_memory->bit_width) { - case 1: - case 2: - case 4: - case 8: - callbacks->MemoryReadUnsafe(src_address, ®isters[ldr_memory->reg_index], - ldr_memory->bit_width); - break; - } - } else if (auto str_static = std::get_if(&cur_opcode.opcode)) { - // Calculate address. - u64 dst_address = registers[str_static->reg_index]; - u64 dst_value = str_static->value; - if (str_static->add_offset_reg) { - dst_address += registers[str_static->offset_reg_index]; - } - // Write value to memory. Gateway only writes on valid bitwidth. - switch (str_static->bit_width) { - case 1: - case 2: - case 4: - case 8: - callbacks->MemoryWriteUnsafe(dst_address, &dst_value, str_static->bit_width); - break; - } - // Increment register if relevant. - if (str_static->increment_reg) { - registers[str_static->reg_index] += str_static->bit_width; - } - } else if (auto perform_math_static = - std::get_if(&cur_opcode.opcode)) { - // Do requested math. - switch (perform_math_static->math_type) { - case RegisterArithmeticType::Addition: - registers[perform_math_static->reg_index] += - static_cast(perform_math_static->value); - break; - case RegisterArithmeticType::Subtraction: - registers[perform_math_static->reg_index] -= - static_cast(perform_math_static->value); - break; - case RegisterArithmeticType::Multiplication: - registers[perform_math_static->reg_index] *= - static_cast(perform_math_static->value); - break; - case RegisterArithmeticType::LeftShift: - registers[perform_math_static->reg_index] <<= - static_cast(perform_math_static->value); - break; - case RegisterArithmeticType::RightShift: - registers[perform_math_static->reg_index] >>= - static_cast(perform_math_static->value); - break; - default: - // Do not handle extensions here. - break; - } - // Apply bit width. - switch (perform_math_static->bit_width) { - case 1: - registers[perform_math_static->reg_index] = - static_cast(registers[perform_math_static->reg_index]); - break; - case 2: - registers[perform_math_static->reg_index] = - static_cast(registers[perform_math_static->reg_index]); - break; - case 4: - registers[perform_math_static->reg_index] = - static_cast(registers[perform_math_static->reg_index]); - break; - case 8: - registers[perform_math_static->reg_index] = - static_cast(registers[perform_math_static->reg_index]); - break; - } - } else if (auto begin_keypress_cond = - std::get_if(&cur_opcode.opcode)) { - // Check for keypress. - if ((begin_keypress_cond->key_mask & kDown) != begin_keypress_cond->key_mask) { - // Keys not pressed. Skip conditional block. - SkipConditionalBlock(true); - } - } else if (auto perform_math_reg = - std::get_if(&cur_opcode.opcode)) { - const u64 operand_1_value = registers[perform_math_reg->src_reg_1_index]; - const u64 operand_2_value = - perform_math_reg->has_immediate - ? GetVmInt(perform_math_reg->value, perform_math_reg->bit_width) - : registers[perform_math_reg->src_reg_2_index]; - - u64 res_val = 0; - // Do requested math. - switch (perform_math_reg->math_type) { - case RegisterArithmeticType::Addition: - res_val = operand_1_value + operand_2_value; - break; - case RegisterArithmeticType::Subtraction: - res_val = operand_1_value - operand_2_value; - break; - case RegisterArithmeticType::Multiplication: - res_val = operand_1_value * operand_2_value; - break; - case RegisterArithmeticType::LeftShift: - res_val = operand_1_value << operand_2_value; - break; - case RegisterArithmeticType::RightShift: - res_val = operand_1_value >> operand_2_value; - break; - case RegisterArithmeticType::LogicalAnd: - res_val = operand_1_value & operand_2_value; - break; - case RegisterArithmeticType::LogicalOr: - res_val = operand_1_value | operand_2_value; - break; - case RegisterArithmeticType::LogicalNot: - res_val = ~operand_1_value; - break; - case RegisterArithmeticType::LogicalXor: - res_val = operand_1_value ^ operand_2_value; - break; - case RegisterArithmeticType::None: - res_val = operand_1_value; - break; - } - - // Apply bit width. - switch (perform_math_reg->bit_width) { - case 1: - res_val = static_cast(res_val); - break; - case 2: - res_val = static_cast(res_val); - break; - case 4: - res_val = static_cast(res_val); - break; - case 8: - res_val = static_cast(res_val); - break; - } - - // Save to register. - registers[perform_math_reg->dst_reg_index] = res_val; - } else if (auto str_register = - std::get_if(&cur_opcode.opcode)) { - // Calculate address. - u64 dst_value = registers[str_register->str_reg_index]; - u64 dst_address = registers[str_register->addr_reg_index]; - switch (str_register->ofs_type) { - case StoreRegisterOffsetType::None: - // Nothing more to do - break; - case StoreRegisterOffsetType::Reg: - dst_address += registers[str_register->ofs_reg_index]; - break; - case StoreRegisterOffsetType::Imm: - dst_address += str_register->rel_address; - break; - case StoreRegisterOffsetType::MemReg: - dst_address = GetCheatProcessAddress(metadata, str_register->mem_type, - registers[str_register->addr_reg_index]); - break; - case StoreRegisterOffsetType::MemImm: - dst_address = GetCheatProcessAddress(metadata, str_register->mem_type, - str_register->rel_address); - break; - case StoreRegisterOffsetType::MemImmReg: - dst_address = GetCheatProcessAddress(metadata, str_register->mem_type, - registers[str_register->addr_reg_index] + - str_register->rel_address); - break; - } - - // Write value to memory. Write only on valid bitwidth. - switch (str_register->bit_width) { - case 1: - case 2: - case 4: - case 8: - callbacks->MemoryWriteUnsafe(dst_address, &dst_value, str_register->bit_width); - break; - } - - // Increment register if relevant. - if (str_register->increment_reg) { - registers[str_register->addr_reg_index] += str_register->bit_width; - } - } else if (auto begin_reg_cond = - std::get_if(&cur_opcode.opcode)) { - // Get value from register. - u64 src_value = 0; - switch (begin_reg_cond->bit_width) { - case 1: - src_value = static_cast(registers[begin_reg_cond->val_reg_index] & 0xFFul); - break; - case 2: - src_value = static_cast(registers[begin_reg_cond->val_reg_index] & 0xFFFFul); - break; - case 4: - src_value = - static_cast(registers[begin_reg_cond->val_reg_index] & 0xFFFFFFFFul); - break; - case 8: - src_value = static_cast(registers[begin_reg_cond->val_reg_index] & - 0xFFFFFFFFFFFFFFFFul); - break; - } - - // Read value from memory. - u64 cond_value = 0; - if (begin_reg_cond->comp_type == CompareRegisterValueType::StaticValue) { - cond_value = GetVmInt(begin_reg_cond->value, begin_reg_cond->bit_width); - } else if (begin_reg_cond->comp_type == CompareRegisterValueType::OtherRegister) { - switch (begin_reg_cond->bit_width) { - case 1: - cond_value = - static_cast(registers[begin_reg_cond->other_reg_index] & 0xFFul); - break; - case 2: - cond_value = - static_cast(registers[begin_reg_cond->other_reg_index] & 0xFFFFul); - break; - case 4: - cond_value = - static_cast(registers[begin_reg_cond->other_reg_index] & 0xFFFFFFFFul); - break; - case 8: - cond_value = static_cast(registers[begin_reg_cond->other_reg_index] & - 0xFFFFFFFFFFFFFFFFul); - break; - } - } else { - u64 cond_address = 0; - switch (begin_reg_cond->comp_type) { - case CompareRegisterValueType::MemoryRelAddr: - cond_address = GetCheatProcessAddress(metadata, begin_reg_cond->mem_type, - begin_reg_cond->rel_address); - break; - case CompareRegisterValueType::MemoryOfsReg: - cond_address = GetCheatProcessAddress(metadata, begin_reg_cond->mem_type, - registers[begin_reg_cond->ofs_reg_index]); - break; - case CompareRegisterValueType::RegisterRelAddr: - cond_address = - registers[begin_reg_cond->addr_reg_index] + begin_reg_cond->rel_address; - break; - case CompareRegisterValueType::RegisterOfsReg: - cond_address = registers[begin_reg_cond->addr_reg_index] + - registers[begin_reg_cond->ofs_reg_index]; - break; - default: - break; - } - switch (begin_reg_cond->bit_width) { - case 1: - case 2: - case 4: - case 8: - callbacks->MemoryReadUnsafe(cond_address, &cond_value, - begin_reg_cond->bit_width); - break; - } - } - - // Check against condition. - bool cond_met = false; - switch (begin_reg_cond->cond_type) { - case ConditionalComparisonType::GT: - cond_met = src_value > cond_value; - break; - case ConditionalComparisonType::GE: - cond_met = src_value >= cond_value; - break; - case ConditionalComparisonType::LT: - cond_met = src_value < cond_value; - break; - case ConditionalComparisonType::LE: - cond_met = src_value <= cond_value; - break; - case ConditionalComparisonType::EQ: - cond_met = src_value == cond_value; - break; - case ConditionalComparisonType::NE: - cond_met = src_value != cond_value; - break; - } - - // Skip conditional block if condition not met. - if (!cond_met) { - SkipConditionalBlock(true); - } - } else if (auto save_restore_reg = - std::get_if(&cur_opcode.opcode)) { - // Save or restore a register. - switch (save_restore_reg->op_type) { - case SaveRestoreRegisterOpType::ClearRegs: - registers[save_restore_reg->dst_index] = 0ul; - break; - case SaveRestoreRegisterOpType::ClearSaved: - saved_values[save_restore_reg->dst_index] = 0ul; - break; - case SaveRestoreRegisterOpType::Save: - saved_values[save_restore_reg->dst_index] = registers[save_restore_reg->src_index]; - break; - case SaveRestoreRegisterOpType::Restore: - default: - registers[save_restore_reg->dst_index] = saved_values[save_restore_reg->src_index]; - break; - } - } else if (auto save_restore_regmask = - std::get_if(&cur_opcode.opcode)) { - // Save or restore register mask. - u64* src; - u64* dst; - switch (save_restore_regmask->op_type) { - case SaveRestoreRegisterOpType::ClearSaved: - case SaveRestoreRegisterOpType::Save: - src = registers.data(); - dst = saved_values.data(); - break; - case SaveRestoreRegisterOpType::ClearRegs: - case SaveRestoreRegisterOpType::Restore: - default: - src = saved_values.data(); - dst = registers.data(); - break; - } - for (std::size_t i = 0; i < NumRegisters; i++) { - if (save_restore_regmask->should_operate[i]) { - switch (save_restore_regmask->op_type) { - case SaveRestoreRegisterOpType::ClearSaved: - case SaveRestoreRegisterOpType::ClearRegs: - dst[i] = 0ul; - break; - case SaveRestoreRegisterOpType::Save: - case SaveRestoreRegisterOpType::Restore: - default: - dst[i] = src[i]; - break; - } - } - } - } else if (auto rw_static_reg = - std::get_if(&cur_opcode.opcode)) { - if (rw_static_reg->static_idx < NumReadableStaticRegisters) { - // Load a register with a static register. - registers[rw_static_reg->idx] = static_registers[rw_static_reg->static_idx]; - } else { - // Store a register to a static register. - static_registers[rw_static_reg->static_idx] = registers[rw_static_reg->idx]; - } - } else if (std::holds_alternative(cur_opcode.opcode)) { - callbacks->PauseProcess(); - } else if (std::holds_alternative(cur_opcode.opcode)) { - callbacks->ResumeProcess(); - } else if (auto debug_log = std::get_if(&cur_opcode.opcode)) { - // Read value from memory. - u64 log_value = 0; - if (debug_log->val_type == DebugLogValueType::RegisterValue) { - switch (debug_log->bit_width) { - case 1: - log_value = static_cast(registers[debug_log->val_reg_index] & 0xFFul); - break; - case 2: - log_value = static_cast(registers[debug_log->val_reg_index] & 0xFFFFul); - break; - case 4: - log_value = - static_cast(registers[debug_log->val_reg_index] & 0xFFFFFFFFul); - break; - case 8: - log_value = static_cast(registers[debug_log->val_reg_index] & - 0xFFFFFFFFFFFFFFFFul); - break; - } - } else { - u64 val_address = 0; - switch (debug_log->val_type) { - case DebugLogValueType::MemoryRelAddr: - val_address = GetCheatProcessAddress(metadata, debug_log->mem_type, - debug_log->rel_address); - break; - case DebugLogValueType::MemoryOfsReg: - val_address = GetCheatProcessAddress(metadata, debug_log->mem_type, - registers[debug_log->ofs_reg_index]); - break; - case DebugLogValueType::RegisterRelAddr: - val_address = registers[debug_log->addr_reg_index] + debug_log->rel_address; - break; - case DebugLogValueType::RegisterOfsReg: - val_address = - registers[debug_log->addr_reg_index] + registers[debug_log->ofs_reg_index]; - break; - default: - break; - } - switch (debug_log->bit_width) { - case 1: - case 2: - case 4: - case 8: - callbacks->MemoryReadUnsafe(val_address, &log_value, debug_log->bit_width); - break; - } - } - - // Log value. - DebugLog(debug_log->log_id, log_value); - } - } -} - -} // namespace Core::Memory diff --git a/src/core/memory/dmnt_cheat_vm.h b/src/core/memory/dmnt_cheat_vm.h deleted file mode 100644 index de5e81add2..0000000000 --- a/src/core/memory/dmnt_cheat_vm.h +++ /dev/null @@ -1,330 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2025 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 - -#include -#include "common/common_types.h" -#include "core/memory/dmnt_cheat_types.h" - -namespace Core::Memory { - -enum class CheatVmOpcodeType : u32 { - StoreStatic = 0, - BeginConditionalBlock = 1, - EndConditionalBlock = 2, - ControlLoop = 3, - LoadRegisterStatic = 4, - LoadRegisterMemory = 5, - StoreStaticToAddress = 6, - PerformArithmeticStatic = 7, - BeginKeypressConditionalBlock = 8, - - // These are not implemented by Gateway's VM. - PerformArithmeticRegister = 9, - StoreRegisterToAddress = 10, - Reserved11 = 11, - - // This is a meta entry, and not a real opcode. - // This is to facilitate multi-nybble instruction decoding. - ExtendedWidth = 12, - - // Extended width opcodes. - BeginRegisterConditionalBlock = 0xC0, - SaveRestoreRegister = 0xC1, - SaveRestoreRegisterMask = 0xC2, - ReadWriteStaticRegister = 0xC3, - - // This is a meta entry, and not a real opcode. - // This is to facilitate multi-nybble instruction decoding. - DoubleExtendedWidth = 0xF0, - - // Double-extended width opcodes. - PauseProcess = 0xFF0, - ResumeProcess = 0xFF1, - DebugLog = 0xFFF, -}; - -enum class MemoryAccessType : u32 { - MainNso = 0, - Heap = 1, - Alias = 2, - Aslr = 3, -}; - -enum class ConditionalComparisonType : u32 { - GT = 1, - GE = 2, - LT = 3, - LE = 4, - EQ = 5, - NE = 6, -}; - -enum class RegisterArithmeticType : u32 { - Addition = 0, - Subtraction = 1, - Multiplication = 2, - LeftShift = 3, - RightShift = 4, - - // These are not supported by Gateway's VM. - LogicalAnd = 5, - LogicalOr = 6, - LogicalNot = 7, - LogicalXor = 8, - - None = 9, -}; - -enum class StoreRegisterOffsetType : u32 { - None = 0, - Reg = 1, - Imm = 2, - MemReg = 3, - MemImm = 4, - MemImmReg = 5, -}; - -enum class CompareRegisterValueType : u32 { - MemoryRelAddr = 0, - MemoryOfsReg = 1, - RegisterRelAddr = 2, - RegisterOfsReg = 3, - StaticValue = 4, - OtherRegister = 5, -}; - -enum class SaveRestoreRegisterOpType : u32 { - Restore = 0, - Save = 1, - ClearSaved = 2, - ClearRegs = 3, -}; - -enum class DebugLogValueType : u32 { - MemoryRelAddr = 0, - MemoryOfsReg = 1, - RegisterRelAddr = 2, - RegisterOfsReg = 3, - RegisterValue = 4, -}; - -union VmInt { - u8 bit8; - u16 bit16; - u32 bit32; - u64 bit64; -}; - -struct StoreStaticOpcode { - u32 bit_width{}; - MemoryAccessType mem_type{}; - u32 offset_register{}; - u64 rel_address{}; - VmInt value{}; -}; - -struct BeginConditionalOpcode { - u32 bit_width{}; - MemoryAccessType mem_type{}; - ConditionalComparisonType cond_type{}; - u64 rel_address{}; - VmInt value{}; -}; - -struct EndConditionalOpcode { - bool is_else; -}; - -struct ControlLoopOpcode { - bool start_loop{}; - u32 reg_index{}; - u32 num_iters{}; -}; - -struct LoadRegisterStaticOpcode { - u32 reg_index{}; - u64 value{}; -}; - -struct LoadRegisterMemoryOpcode { - u32 bit_width{}; - MemoryAccessType mem_type{}; - u32 reg_index{}; - bool load_from_reg{}; - u64 rel_address{}; -}; - -struct StoreStaticToAddressOpcode { - u32 bit_width{}; - u32 reg_index{}; - bool increment_reg{}; - bool add_offset_reg{}; - u32 offset_reg_index{}; - u64 value{}; -}; - -struct PerformArithmeticStaticOpcode { - u32 bit_width{}; - u32 reg_index{}; - RegisterArithmeticType math_type{}; - u32 value{}; -}; - -struct BeginKeypressConditionalOpcode { - u32 key_mask{}; -}; - -struct PerformArithmeticRegisterOpcode { - u32 bit_width{}; - RegisterArithmeticType math_type{}; - u32 dst_reg_index{}; - u32 src_reg_1_index{}; - u32 src_reg_2_index{}; - bool has_immediate{}; - VmInt value{}; -}; - -struct StoreRegisterToAddressOpcode { - u32 bit_width{}; - u32 str_reg_index{}; - u32 addr_reg_index{}; - bool increment_reg{}; - StoreRegisterOffsetType ofs_type{}; - MemoryAccessType mem_type{}; - u32 ofs_reg_index{}; - u64 rel_address{}; -}; - -struct BeginRegisterConditionalOpcode { - u32 bit_width{}; - ConditionalComparisonType cond_type{}; - u32 val_reg_index{}; - CompareRegisterValueType comp_type{}; - MemoryAccessType mem_type{}; - u32 addr_reg_index{}; - u32 other_reg_index{}; - u32 ofs_reg_index{}; - u64 rel_address{}; - VmInt value{}; -}; - -struct SaveRestoreRegisterOpcode { - u32 dst_index{}; - u32 src_index{}; - SaveRestoreRegisterOpType op_type{}; -}; - -struct SaveRestoreRegisterMaskOpcode { - SaveRestoreRegisterOpType op_type{}; - std::array should_operate{}; -}; - -struct ReadWriteStaticRegisterOpcode { - u32 static_idx{}; - u32 idx{}; -}; - -struct PauseProcessOpcode {}; - -struct ResumeProcessOpcode {}; - -struct DebugLogOpcode { - u32 bit_width{}; - u32 log_id{}; - DebugLogValueType val_type{}; - MemoryAccessType mem_type{}; - u32 addr_reg_index{}; - u32 val_reg_index{}; - u32 ofs_reg_index{}; - u64 rel_address{}; -}; - -struct UnrecognizedInstruction { - CheatVmOpcodeType opcode{}; -}; - -struct CheatVmOpcode { - bool begin_conditional_block{}; - std::variant - opcode{}; -}; - -class DmntCheatVm { -public: - /// Helper Type for DmntCheatVm <=> yuzu Interface - class Callbacks { - public: - virtual ~Callbacks(); - - virtual void MemoryReadUnsafe(VAddr address, void* data, u64 size) = 0; - virtual void MemoryWriteUnsafe(VAddr address, const void* data, u64 size) = 0; - - virtual u64 HidKeysDown() = 0; - - virtual void PauseProcess() = 0; - virtual void ResumeProcess() = 0; - - virtual void DebugLog(u8 id, u64 value) = 0; - virtual void CommandLog(std::string_view data) = 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; - static constexpr std::size_t NumStaticRegisters = - NumReadableStaticRegisters + NumWritableStaticRegisters; - - explicit DmntCheatVm(std::unique_ptr callbacks_); - ~DmntCheatVm(); - - std::size_t GetProgramSize() const { - return this->num_opcodes; - } - - bool LoadProgram(const std::vector& cheats); - void Execute(const CheatProcessMetadata& metadata); - -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::array registers{}; - std::array saved_values{}; - std::array static_registers{}; - std::array loop_tops{}; - - bool DecodeNextOpcode(CheatVmOpcode& out); - void SkipConditionalBlock(bool is_if); - void ResetState(); - - // For implementing the DebugLog opcode. - void DebugLog(u32 log_id, u64 value); - - void LogOpcode(const CheatVmOpcode& opcode); - - static u64 GetVmInt(VmInt value, u32 bit_width); - static u64 GetCheatProcessAddress(const CheatProcessMetadata& metadata, - MemoryAccessType mem_type, u64 rel_address); -}; - -}; // namespace Core::Memory diff --git a/src/video_core/vulkan_common/vulkan_instance.cpp b/src/video_core/vulkan_common/vulkan_instance.cpp index ebb3fcd020..022e27a75f 100644 --- a/src/video_core/vulkan_common/vulkan_instance.cpp +++ b/src/video_core/vulkan_common/vulkan_instance.cpp @@ -79,9 +79,7 @@ namespace { #endif if (enable_validation && AreExtensionsSupported(dld, *properties, std::array{VK_EXT_DEBUG_UTILS_EXTENSION_NAME})) extensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME); - // VK_EXT_surface_maintenance1 is required for VK_EXT_swapchain_maintenance1 - if (window_type != Core::Frontend::WindowSystemType::Headless && AreExtensionsSupported(dld, *properties, std::array{VK_EXT_SURFACE_MAINTENANCE_1_EXTENSION_NAME})) - extensions.push_back(VK_EXT_SURFACE_MAINTENANCE_1_EXTENSION_NAME); + } return extensions; } diff --git a/src/yuzu/configuration/configure_per_game_addons.cpp b/src/yuzu/configuration/configure_per_game_addons.cpp index ee2db55a5d..7fd975164d 100644 --- a/src/yuzu/configuration/configure_per_game_addons.cpp +++ b/src/yuzu/configuration/configure_per_game_addons.cpp @@ -5,6 +5,8 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include +#include +#include #include #include @@ -73,10 +75,32 @@ ConfigurePerGameAddons::~ConfigurePerGameAddons() = default; void ConfigurePerGameAddons::ApplyConfiguration() { std::vector disabled_addons; - for (const auto& item : list_items) { - const auto disabled = item.front()->checkState() == Qt::Unchecked; - if (disabled) - disabled_addons.push_back(item.front()->text().toStdString()); + // Helper function to recursively collect disabled items + std::function collect_disabled = [&](QStandardItem* item) { + if (item == nullptr) { + return; + } + + // Check if this item is disabled + if (item->isCheckable() && item->checkState() == Qt::Unchecked) { + // Use the stored key from UserRole, falling back to text + const auto key = item->data(Qt::UserRole).toString(); + if (!key.isEmpty()) { + disabled_addons.push_back(key.toStdString()); + } else { + disabled_addons.push_back(item->text().toStdString()); + } + } + + // Process children (for cheats under mods) + for (int row = 0; row < item->rowCount(); ++row) { + collect_disabled(item->child(row, 0)); + } + }; + + // Process all root items + for (int row = 0; row < item_model->rowCount(); ++row) { + collect_disabled(item_model->item(row, 0)); } auto current = Settings::values.disabled_addons[title_id]; @@ -123,24 +147,61 @@ void ConfigurePerGameAddons::LoadConfiguration() { FileSys::VirtualFile update_raw; loader->ReadUpdateRaw(update_raw); + // Get the build ID from the main executable for cheat enumeration + const auto build_id = pm.GetBuildID(update_raw); + const auto& disabled = Settings::values.disabled_addons[title_id]; - for (const auto& patch : pm.GetPatches(update_raw)) { + // Map to store parent items for mods (for adding cheat children) + std::map mod_items; + + for (const auto& patch : pm.GetPatches(update_raw, build_id)) { const auto name = QString::fromStdString(patch.name); + // For cheats, we need to use the full key (parent::name) for storage + std::string storage_key; + if (patch.type == FileSys::PatchType::Cheat && !patch.parent_name.empty()) { + storage_key = patch.parent_name + "::" + patch.name; + } else { + storage_key = patch.name; + } + auto* const first_item = new QStandardItem; first_item->setText(name); first_item->setCheckable(true); + // Store the storage key as user data for later retrieval + first_item->setData(QString::fromStdString(storage_key), Qt::UserRole); + const auto patch_disabled = - std::find(disabled.begin(), disabled.end(), name.toStdString()) != disabled.end(); + std::find(disabled.begin(), disabled.end(), storage_key) != disabled.end(); first_item->setCheckState(patch_disabled ? Qt::Unchecked : Qt::Checked); - list_items.push_back(QList{ - first_item, new QStandardItem{QString::fromStdString(patch.version)}}); - item_model->appendRow(list_items.back()); + auto* const version_item = new QStandardItem{QString::fromStdString(patch.version)}; + + if (patch.type == FileSys::PatchType::Cheat && !patch.parent_name.empty()) { + // This is a cheat - add as child of its parent mod + auto parent_it = mod_items.find(patch.parent_name); + if (parent_it != mod_items.end()) { + parent_it->second->appendRow(QList{first_item, version_item}); + } else { + // Parent not found (shouldn't happen), add to root + list_items.push_back(QList{first_item, version_item}); + item_model->appendRow(list_items.back()); + } + } else { + // This is a top-level item (Update, Mod, DLC) + list_items.push_back(QList{first_item, version_item}); + item_model->appendRow(list_items.back()); + + // Store mod items for later cheat attachment + if (patch.type == FileSys::PatchType::Mod) { + mod_items[patch.name] = first_item; + } + } } + tree_view->expandAll(); tree_view->resizeColumnToContents(1); }