mirror of
https://github.com/dolphin-emu/dolphin.git
synced 2024-11-15 05:47:56 -07:00
f3b8a985e7
These games are erroneously zeroing buffers before they can be fully copied to ARAM by DMA. The responsible memset() calls are followed by a call to DVDRead() which issues dcbi instructions that effectively cancel the memset() on real hardware. Because Dolphin lacks dcache emulation, the effects of the memset() calls are observed, which causes missing audio. In a comment on the original bug, phire noted that the issue can be corrected by simply nop'ing out the offending memset() calls. Because the games dynamically load different .rel executables based on the character and/or language, the addresses of these calls can vary. To deal generally with the problem of code being dynamically loaded to fixed, known addresses, the patch engine is extended to support conditional patches which require a match against a known value. This sort of thing is already achievable with Action Replay/Gecko codes, but their use depends on enabling cheats globally in Dolphin, which is not a prerequisite shared by patches. Patches are included for every region, character, and language combination. They are enabled by default. The end result is an approximation of the games' behavior on real hardware without the associated complexity of proper dcache emulation. https://bugs.dolphin-emu.org/issues/9840
199 lines
5.2 KiB
C++
199 lines
5.2 KiB
C++
// Copyright 2018 Dolphin Emulator Project
|
|
// Licensed under GPLv2+
|
|
// Refer to the license.txt file included.
|
|
|
|
#include "DolphinQt/Config/PatchesWidget.h"
|
|
|
|
#include <QGridLayout>
|
|
#include <QListWidget>
|
|
#include <QPushButton>
|
|
|
|
#include <fmt/format.h>
|
|
|
|
#include "Common/FileUtil.h"
|
|
#include "Common/IniFile.h"
|
|
#include "Common/StringUtil.h"
|
|
|
|
#include "Core/ConfigManager.h"
|
|
#include "Core/PatchEngine.h"
|
|
|
|
#include "DolphinQt/Config/NewPatchDialog.h"
|
|
|
|
#include "UICommon/GameFile.h"
|
|
|
|
PatchesWidget::PatchesWidget(const UICommon::GameFile& game)
|
|
: m_game_id(game.GetGameID()), m_game_revision(game.GetRevision())
|
|
{
|
|
IniFile game_ini_local;
|
|
game_ini_local.Load(File::GetUserPath(D_GAMESETTINGS_IDX) + m_game_id + ".ini");
|
|
|
|
IniFile game_ini_default = SConfig::GetInstance().LoadDefaultGameIni(m_game_id, m_game_revision);
|
|
|
|
PatchEngine::LoadPatchSection("OnFrame", m_patches, game_ini_default, game_ini_local);
|
|
|
|
CreateWidgets();
|
|
ConnectWidgets();
|
|
|
|
Update();
|
|
|
|
UpdateActions();
|
|
}
|
|
|
|
void PatchesWidget::CreateWidgets()
|
|
{
|
|
m_list = new QListWidget;
|
|
m_add_button = new QPushButton(tr("&Add..."));
|
|
m_edit_button = new QPushButton();
|
|
m_remove_button = new QPushButton(tr("&Remove"));
|
|
|
|
auto* layout = new QGridLayout;
|
|
|
|
layout->addWidget(m_list, 0, 0, 1, -1);
|
|
layout->addWidget(m_add_button, 1, 0);
|
|
layout->addWidget(m_edit_button, 1, 2);
|
|
layout->addWidget(m_remove_button, 1, 1);
|
|
|
|
setLayout(layout);
|
|
}
|
|
|
|
void PatchesWidget::ConnectWidgets()
|
|
{
|
|
connect(m_list, &QListWidget::itemSelectionChanged, this, &PatchesWidget::UpdateActions);
|
|
connect(m_list, &QListWidget::itemChanged, this, &PatchesWidget::OnItemChanged);
|
|
connect(m_remove_button, &QPushButton::clicked, this, &PatchesWidget::OnRemove);
|
|
connect(m_add_button, &QPushButton::clicked, this, &PatchesWidget::OnAdd);
|
|
connect(m_edit_button, &QPushButton::clicked, this, &PatchesWidget::OnEdit);
|
|
}
|
|
|
|
void PatchesWidget::OnItemChanged(QListWidgetItem* item)
|
|
{
|
|
m_patches[m_list->row(item)].enabled = (item->checkState() == Qt::Checked);
|
|
SavePatches();
|
|
}
|
|
|
|
void PatchesWidget::OnAdd()
|
|
{
|
|
PatchEngine::Patch patch;
|
|
patch.user_defined = true;
|
|
|
|
if (NewPatchDialog(this, patch).exec())
|
|
{
|
|
m_patches.push_back(patch);
|
|
SavePatches();
|
|
Update();
|
|
}
|
|
}
|
|
|
|
void PatchesWidget::OnEdit()
|
|
{
|
|
if (m_list->selectedItems().isEmpty())
|
|
return;
|
|
|
|
auto* item = m_list->selectedItems()[0];
|
|
|
|
auto patch = m_patches[m_list->row(item)];
|
|
|
|
if (!patch.user_defined)
|
|
{
|
|
// i18n: If there is a pre-defined patch with the name %1 and the user wants to edit it,
|
|
// a copy of it gets created with this name
|
|
patch.name = tr("%1 (Copy)").arg(QString::fromStdString(patch.name)).toStdString();
|
|
}
|
|
|
|
if (NewPatchDialog(this, patch).exec())
|
|
{
|
|
if (patch.user_defined)
|
|
{
|
|
m_patches[m_list->row(item)] = patch;
|
|
}
|
|
else
|
|
{
|
|
patch.user_defined = true;
|
|
m_patches.push_back(patch);
|
|
}
|
|
|
|
SavePatches();
|
|
Update();
|
|
}
|
|
}
|
|
|
|
void PatchesWidget::OnRemove()
|
|
{
|
|
if (m_list->selectedItems().isEmpty())
|
|
return;
|
|
|
|
m_patches.erase(m_patches.begin() + m_list->row(m_list->selectedItems()[0]));
|
|
SavePatches();
|
|
Update();
|
|
}
|
|
|
|
void PatchesWidget::SavePatches()
|
|
{
|
|
std::vector<std::string> lines;
|
|
std::vector<std::string> lines_enabled;
|
|
std::vector<std::string> lines_disabled;
|
|
|
|
for (const auto& patch : m_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 auto& entry : patch.entries)
|
|
{
|
|
if (!entry.conditional)
|
|
{
|
|
lines.emplace_back(fmt::format("0x{:08X}:{}:0x{:08X}", entry.address,
|
|
PatchEngine::PatchTypeAsString(entry.type), entry.value));
|
|
}
|
|
else
|
|
{
|
|
lines.emplace_back(fmt::format("0x{:08X}:{}:0x{:08X}:0x{:08X}", entry.address,
|
|
PatchEngine::PatchTypeAsString(entry.type), entry.value,
|
|
entry.comparand));
|
|
}
|
|
}
|
|
}
|
|
|
|
IniFile game_ini_local;
|
|
game_ini_local.Load(File::GetUserPath(D_GAMESETTINGS_IDX) + m_game_id + ".ini");
|
|
|
|
game_ini_local.SetLines("OnFrame_Enabled", lines_enabled);
|
|
game_ini_local.SetLines("OnFrame_Disabled", lines_disabled);
|
|
game_ini_local.SetLines("OnFrame", lines);
|
|
|
|
game_ini_local.Save(File::GetUserPath(D_GAMESETTINGS_IDX) + m_game_id + ".ini");
|
|
}
|
|
|
|
void PatchesWidget::Update()
|
|
{
|
|
m_list->clear();
|
|
|
|
for (const auto& patch : m_patches)
|
|
{
|
|
auto* item = new QListWidgetItem(QString::fromStdString(patch.name));
|
|
item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
|
|
item->setCheckState(patch.enabled ? Qt::Checked : Qt::Unchecked);
|
|
item->setData(Qt::UserRole, patch.user_defined);
|
|
|
|
m_list->addItem(item);
|
|
}
|
|
}
|
|
|
|
void PatchesWidget::UpdateActions()
|
|
{
|
|
bool selected = !m_list->selectedItems().isEmpty();
|
|
|
|
auto* item = selected ? m_list->selectedItems()[0] : nullptr;
|
|
|
|
bool user_defined = selected ? item->data(Qt::UserRole).toBool() : true;
|
|
|
|
m_edit_button->setEnabled(selected);
|
|
m_edit_button->setText(user_defined ? tr("&Edit...") : tr("&Clone..."));
|
|
m_remove_button->setEnabled(selected && user_defined);
|
|
}
|