dolphin/Source/Core/Core/PatchEngine.cpp

347 lines
9.6 KiB
C++

// Copyright 2008 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
// PatchEngine
// Supports simple memory patches, and has a partial Action Replay implementation
// in ActionReplay.cpp/h.
#include "Core/PatchEngine.h"
#include <algorithm>
#include <array>
#include <iterator>
#include <mutex>
#include <optional>
#include <span>
#include <string>
#include <vector>
#include <fmt/format.h>
#include "Common/Assert.h"
#include "Common/Debug/MemoryPatches.h"
#include "Common/IniFile.h"
#include "Common/StringUtil.h"
#include "Core/AchievementManager.h"
#include "Core/ActionReplay.h"
#include "Core/CheatCodes.h"
#include "Core/Config/SessionSettings.h"
#include "Core/ConfigManager.h"
#include "Core/Core.h"
#include "Core/Debugger/PPCDebugInterface.h"
#include "Core/GeckoCode.h"
#include "Core/GeckoCodeConfig.h"
#include "Core/PowerPC/MMU.h"
#include "Core/PowerPC/PowerPC.h"
#include "Core/System.h"
namespace PatchEngine
{
constexpr std::array<const char*, 3> s_patch_type_strings{{
"byte",
"word",
"dword",
}};
static std::vector<Patch> s_on_frame;
static std::vector<std::size_t> s_on_frame_memory;
static std::mutex s_on_frame_memory_mutex;
const char* PatchTypeAsString(PatchType type)
{
return s_patch_type_strings.at(static_cast<int>(type));
}
std::optional<PatchEntry> DeserializeLine(std::string line)
{
std::string::size_type loc = line.find('=');
if (loc != std::string::npos)
line[loc] = ':';
const std::vector<std::string> items = SplitString(line, ':');
PatchEntry entry;
if (items.size() < 3)
return std::nullopt;
if (!TryParse(items[0], &entry.address))
return std::nullopt;
if (!TryParse(items[2], &entry.value))
return std::nullopt;
if (items.size() >= 4)
{
if (!TryParse(items[3], &entry.comparand))
return std::nullopt;
entry.conditional = true;
}
const auto iter = std::ranges::find(s_patch_type_strings, items[1]);
if (iter == s_patch_type_strings.end())
return std::nullopt;
entry.type = static_cast<PatchType>(std::distance(s_patch_type_strings.begin(), iter));
return entry;
}
std::string SerializeLine(const PatchEntry& entry)
{
if (entry.conditional)
{
return fmt::format("0x{:08X}:{}:0x{:08X}:0x{:08X}", entry.address,
PatchEngine::PatchTypeAsString(entry.type), entry.value, entry.comparand);
}
else
{
return fmt::format("0x{:08X}:{}:0x{:08X}", entry.address,
PatchEngine::PatchTypeAsString(entry.type), entry.value);
}
}
void LoadPatchSection(const std::string& section, std::vector<Patch>* patches,
const Common::IniFile& globalIni, const Common::IniFile& localIni)
{
for (const auto* ini : {&globalIni, &localIni})
{
std::vector<std::string> lines;
Patch currentPatch;
ini->GetLines(section, &lines);
for (std::string& line : lines)
{
if (line.empty())
continue;
if (line[0] == '$')
{
// Take care of the previous code
if (!currentPatch.name.empty())
{
patches->push_back(currentPatch);
}
currentPatch.entries.clear();
// Set name and whether the patch is user defined
currentPatch.name = line.substr(1, line.size() - 1);
currentPatch.user_defined = (ini == &localIni);
}
else
{
if (std::optional<PatchEntry> entry = DeserializeLine(line))
currentPatch.entries.push_back(*entry);
}
}
if (!currentPatch.name.empty() && !currentPatch.entries.empty())
{
patches->push_back(currentPatch);
}
ReadEnabledAndDisabled(*ini, section, patches);
if (ini == &globalIni)
{
for (Patch& patch : *patches)
patch.default_enabled = patch.enabled;
}
}
}
void SavePatchSection(Common::IniFile* local_ini, const std::vector<Patch>& patches)
{
std::vector<std::string> lines;
std::vector<std::string> lines_enabled;
std::vector<std::string> lines_disabled;
for (const auto& patch : patches)
{
if (patch.enabled != patch.default_enabled)
(patch.enabled ? lines_enabled : lines_disabled).emplace_back('$' + patch.name);
if (!patch.user_defined)
continue;
lines.emplace_back('$' + patch.name);
for (const PatchEntry& entry : patch.entries)
lines.emplace_back(SerializeLine(entry));
}
local_ini->SetLines("OnFrame_Enabled", lines_enabled);
local_ini->SetLines("OnFrame_Disabled", lines_disabled);
local_ini->SetLines("OnFrame", lines);
}
void LoadPatches()
{
const auto& sconfig = SConfig::GetInstance();
Common::IniFile merged = sconfig.LoadGameIni();
Common::IniFile globalIni = sconfig.LoadDefaultGameIni();
Common::IniFile localIni = sconfig.LoadLocalGameIni();
LoadPatchSection("OnFrame", &s_on_frame, globalIni, localIni);
#ifdef USE_RETRO_ACHIEVEMENTS
{
std::lock_guard lg{AchievementManager::GetInstance().GetLock()};
AchievementManager::GetInstance().FilterApprovedPatches(s_on_frame, sconfig.GetGameID());
}
#endif // USE_RETRO_ACHIEVEMENTS
// Check if I'm syncing Codes
if (Config::Get(Config::SESSION_CODE_SYNC_OVERRIDE))
{
Gecko::SetSyncedCodesAsActive();
ActionReplay::SetSyncedCodesAsActive();
}
else
{
Gecko::SetActiveCodes(Gecko::LoadCodes(globalIni, localIni), sconfig.GetGameID());
ActionReplay::LoadAndApplyCodes(globalIni, localIni, sconfig.GetGameID());
}
}
static void ApplyPatches(const Core::CPUThreadGuard& guard, const std::vector<Patch>& patches)
{
for (const Patch& patch : patches)
{
if (patch.enabled)
{
for (const PatchEntry& entry : patch.entries)
{
u32 addr = entry.address;
u32 value = entry.value;
u32 comparand = entry.comparand;
switch (entry.type)
{
case PatchType::Patch8Bit:
if (!entry.conditional ||
PowerPC::MMU::HostRead_U8(guard, addr) == static_cast<u8>(comparand))
{
PowerPC::MMU::HostWrite_U8(guard, static_cast<u8>(value), addr);
}
break;
case PatchType::Patch16Bit:
if (!entry.conditional ||
PowerPC::MMU::HostRead_U16(guard, addr) == static_cast<u16>(comparand))
{
PowerPC::MMU::HostWrite_U16(guard, static_cast<u16>(value), addr);
}
break;
case PatchType::Patch32Bit:
if (!entry.conditional || PowerPC::MMU::HostRead_U32(guard, addr) == comparand)
PowerPC::MMU::HostWrite_U32(guard, value, addr);
break;
default:
// unknown patchtype
break;
}
}
}
}
}
static void ApplyMemoryPatches(const Core::CPUThreadGuard& guard,
std::span<const std::size_t> memory_patch_indices)
{
std::lock_guard lock(s_on_frame_memory_mutex);
for (std::size_t index : memory_patch_indices)
{
guard.GetSystem().GetPowerPC().GetDebugInterface().ApplyExistingPatch(guard, index);
}
}
// Requires MSR.DR, MSR.IR
// There's no perfect way to do this, it's just a heuristic.
// We require at least 2 stack frames, if the stack is shallower than that then it won't work.
static bool IsStackValid(const Core::CPUThreadGuard& guard)
{
const auto& ppc_state = guard.GetSystem().GetPPCState();
DEBUG_ASSERT(ppc_state.msr.DR && ppc_state.msr.IR);
// Check the stack pointer
const u32 SP = ppc_state.gpr[1];
if (!PowerPC::MMU::HostIsRAMAddress(guard, SP))
return false;
// Read the frame pointer from the stack (find 2nd frame from top), assert that it makes sense
const u32 next_SP = PowerPC::MMU::HostRead_U32(guard, SP);
if (next_SP <= SP || !PowerPC::MMU::HostIsRAMAddress(guard, next_SP) ||
!PowerPC::MMU::HostIsRAMAddress(guard, next_SP + 4))
{
return false;
}
// Check the link register makes sense (that it points to a valid IBAT address)
const u32 address = PowerPC::MMU::HostRead_U32(guard, next_SP + 4);
return PowerPC::MMU::HostIsInstructionRAMAddress(guard, address) &&
0 != PowerPC::MMU::HostRead_Instruction(guard, address);
}
void AddMemoryPatch(std::size_t index)
{
std::lock_guard lock(s_on_frame_memory_mutex);
s_on_frame_memory.push_back(index);
}
void RemoveMemoryPatch(std::size_t index)
{
std::lock_guard lock(s_on_frame_memory_mutex);
std::erase(s_on_frame_memory, index);
}
static void ApplyStartupPatches(Core::System& system)
{
ASSERT(Core::IsCPUThread());
Core::CPUThreadGuard guard(system);
ApplyPatches(guard, s_on_frame);
}
bool ApplyFramePatches(Core::System& system)
{
const auto& ppc_state = system.GetPPCState();
ASSERT(Core::IsCPUThread());
Core::CPUThreadGuard guard(system);
// Because we're using the VI Interrupt to time this instead of patching the game with a
// callback hook we can end up catching the game in an exception vector.
// We deal with this by returning false so that SystemTimers will reschedule us in a few cycles
// where we can try again after the CPU hopefully returns back to the normal instruction flow.
if (!ppc_state.msr.DR || !ppc_state.msr.IR || !IsStackValid(guard))
{
DEBUG_LOG_FMT(ACTIONREPLAY,
"Need to retry later. CPU configuration is currently incorrect. PC = {:#010x}, "
"MSR = {:#010x}",
ppc_state.pc, ppc_state.msr.Hex);
return false;
}
ApplyPatches(guard, s_on_frame);
ApplyMemoryPatches(guard, s_on_frame_memory);
// Run the Gecko code handler
Gecko::RunCodeHandler(guard);
ActionReplay::RunAllActive(guard);
return true;
}
void Shutdown()
{
s_on_frame.clear();
ActionReplay::ApplyCodes({}, "");
Gecko::Shutdown();
}
void Reload(Core::System& system)
{
Shutdown();
LoadPatches();
ApplyStartupPatches(system);
}
} // namespace PatchEngine