Files
ARMSX2/pcsx2/GameDatabase.cpp
T
Brian Degenhardt 44e950e7d5 GS: expose render-pass coalescing in the settings UI and GameDB
Coalescing has only been reachable by hand-editing the INI. It needs to be
switchable per game, because whether it is worth anything depends entirely on
the title: it pays off when a game alternates between two render targets, and
does nothing at all otherwise.

Add the GameDB key coalesceRenderPasses, so a game that benefits can turn it on
by itself, and a checkbox in Graphics > Advanced plus a Full Screen UI toggle
next to the other driver-level GS options, so it can be tried on anything.

It classifies as a user-hack fix, which means enabling Manual Hardware Renderer
Fixes turns it back off - the usual escape hatch, and worth having while this is
new. Note the consequence for A/B testing: GameDB is applied after settings are
loaded, so for a game carrying the key, -set cannot switch it off.
2026-07-25 18:17:16 -07:00

1407 lines
44 KiB
C++

// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "GameDatabase.h"
#include "GS/GS.h"
#include "Host.h"
#include "IconsFontAwesome.h"
#include "vtlb.h"
#include "common/Console.h"
#include "common/EnumOps.h"
#include "common/Error.h"
#include "common/FileSystem.h"
#include "common/Path.h"
#include "common/StringUtil.h"
#include "common/Timer.h"
#include "common/YAML.h"
#include <sstream>
#include "fmt/format.h"
#include "fmt/ranges.h"
#include <fstream>
#include <mutex>
#include <optional>
namespace GameDatabaseSchema
{
static const char* getHWFixName(GSHWFixId id);
static std::optional<GSHWFixId> parseHWFixName(const std::string_view name);
static bool isUserHackHWFix(GSHWFixId id);
} // namespace GameDatabaseSchema
namespace GameDatabase
{
static void populateEntry(GameDatabaseSchema::GameEntry& gameEntry, const std::string_view serial,
const ryml::NodeRef& node, bool is_override);
static void parseAndInsert(const std::string_view serial, const ryml::NodeRef& node);
static void mergeOverride(const std::string_view serial, const ryml::NodeRef& node);
static void loadFile(const std::string& path, const std::string& name, bool is_override);
static void initDatabase();
} // namespace GameDatabase
static constexpr char GAMEDB_YAML_FILE_NAME[] = "GameIndex.yaml";
// ARMSX2 GameDB overrides — layered on top of upstream GameIndex.yaml after it
// loads. Entries either add NEW serials upstream doesn't cover, or PARTIAL-
// override existing upstream entries: scalar fields replace upstream when
// present; list fields (gameFixes / speedHacks / gsHWFixes / memcardFilters /
// patches / dynaPatches) get cleared when the override specifies them so we
// REPLACE rather than append. The file is optional — missing is a silent skip.
static constexpr char GAMEDB_OVERRIDE_YAML_FILE_NAME[] = "armsx2_overrides.yaml";
static std::unordered_map<std::string, GameDatabaseSchema::GameEntry> s_game_db;
static std::once_flag s_load_once_flag;
std::string GameDatabaseSchema::GameEntry::memcardFiltersAsString() const
{
return fmt::to_string(fmt::join(memcardFilters, "/"));
}
const std::string* GameDatabaseSchema::GameEntry::findPatch(u32 crc) const
{
if (crc == 0)
return nullptr;
auto it = patches.find(crc);
if (it != patches.end())
return &it->second;
it = patches.find(0);
if (it != patches.end())
return &it->second;
return nullptr;
}
const char* GameDatabaseSchema::GameEntry::compatAsString() const
{
switch (compat)
{
case GameDatabaseSchema::Compatibility::Perfect:
return "Perfect";
case GameDatabaseSchema::Compatibility::Playable:
return "Playable";
case GameDatabaseSchema::Compatibility::InGame:
return "In-Game";
case GameDatabaseSchema::Compatibility::Menu:
return "Menu";
case GameDatabaseSchema::Compatibility::Intro:
return "Intro";
case GameDatabaseSchema::Compatibility::Nothing:
return "Nothing";
default:
return "Unknown";
}
}
void GameDatabase::populateEntry(GameDatabaseSchema::GameEntry& gameEntry, const std::string_view serial,
const ryml::NodeRef& node, bool is_override)
{
if (node.has_child("name"))
{
node["name"] >> gameEntry.name;
}
if (node.has_child("name-sort"))
{
node["name-sort"] >> gameEntry.name_sort;
}
if (node.has_child("name-en"))
{
node["name-en"] >> gameEntry.name_en;
}
if (node.has_child("region"))
{
node["region"] >> gameEntry.region;
}
if (node.has_child("compat"))
{
int val = 0;
node["compat"] >> val;
gameEntry.compat = static_cast<GameDatabaseSchema::Compatibility>(val);
}
if (node.has_child("roundModes"))
{
if (node["roundModes"].has_child("eeRoundMode"))
{
int eeVal = -1;
node["roundModes"]["eeRoundMode"] >> eeVal;
if (eeVal >= 0 && eeVal < static_cast<int>(FPRoundMode::MaxCount))
gameEntry.eeRoundMode = static_cast<FPRoundMode>(eeVal);
else
Console.Error(fmt::format("GameDB: Invalid EE round mode '{}', specified for serial: '{}'.", eeVal, serial));
}
if (node["roundModes"].has_child("eeDivRoundMode"))
{
int eeVal = -1;
node["roundModes"]["eeDivRoundMode"] >> eeVal;
if (eeVal >= 0 && eeVal < static_cast<int>(FPRoundMode::MaxCount))
gameEntry.eeDivRoundMode = static_cast<FPRoundMode>(eeVal);
else
Console.Error(fmt::format("GameDB: Invalid EE division round mode '{}', specified for serial: '{}'.", eeVal, serial));
}
if (node["roundModes"].has_child("vuRoundMode"))
{
int vuVal = -1;
node["roundModes"]["vuRoundMode"] >> vuVal;
if (vuVal >= 0 && vuVal < static_cast<int>(FPRoundMode::MaxCount))
{
gameEntry.vu0RoundMode = static_cast<FPRoundMode>(vuVal);
gameEntry.vu1RoundMode = static_cast<FPRoundMode>(vuVal);
}
else
{
Console.Error(fmt::format("GameDB: Invalid VU round mode '{}', specified for serial: '{}'.", vuVal, serial));
}
}
if (node["roundModes"].has_child("vu0RoundMode"))
{
int vuVal = -1;
node["roundModes"]["vu0RoundMode"] >> vuVal;
if (vuVal >= 0 && vuVal < static_cast<int>(FPRoundMode::MaxCount))
gameEntry.vu0RoundMode = static_cast<FPRoundMode>(vuVal);
else
Console.Error(fmt::format("GameDB: Invalid VU0 round mode '{}', specified for serial: '{}'.", vuVal, serial));
}
if (node["roundModes"].has_child("vu1RoundMode"))
{
int vuVal = -1;
node["roundModes"]["vu1RoundMode"] >> vuVal;
if (vuVal >= 0 && vuVal < static_cast<int>(FPRoundMode::MaxCount))
gameEntry.vu1RoundMode = static_cast<FPRoundMode>(vuVal);
else
Console.Error(fmt::format("GameDB: Invalid VU1 round mode '{}', specified for serial: '{}'.", vuVal, serial));
}
}
if (node.has_child("clampModes"))
{
if (node["clampModes"].has_child("eeClampMode"))
{
int eeVal = -1;
node["clampModes"]["eeClampMode"] >> eeVal;
gameEntry.eeClampMode = static_cast<GameDatabaseSchema::ClampMode>(eeVal);
}
if (node["clampModes"].has_child("vuClampMode"))
{
int vuVal = -1;
node["clampModes"]["vuClampMode"] >> vuVal;
gameEntry.vu0ClampMode = static_cast<GameDatabaseSchema::ClampMode>(vuVal);
gameEntry.vu1ClampMode = static_cast<GameDatabaseSchema::ClampMode>(vuVal);
}
if (node["clampModes"].has_child("vu0ClampMode"))
{
int vuVal = -1;
node["clampModes"]["vu0ClampMode"] >> vuVal;
gameEntry.vu0ClampMode = static_cast<GameDatabaseSchema::ClampMode>(vuVal);
}
if (node["clampModes"].has_child("vu1ClampMode"))
{
int vuVal = -1;
node["clampModes"]["vu1ClampMode"] >> vuVal;
gameEntry.vu1ClampMode = static_cast<GameDatabaseSchema::ClampMode>(vuVal);
}
}
// Validate game fixes, invalid ones will be dropped!
if (node.has_child("gameFixes") && node["gameFixes"].has_children())
{
// Override REPLACES upstream's list, it does not append.
if (is_override)
gameEntry.gameFixes.clear();
for (const auto& n : node["gameFixes"].children())
{
bool fixValidated = false;
auto fix = std::string(n.val().str, n.val().len);
// Enum values don't end with Hack, but gamedb does, so remove it before comparing.
if (fix.ends_with("Hack"))
{
fix.erase(fix.size() - 4);
for (GamefixId id = GamefixId_FIRST; id < GamefixId_COUNT; id = static_cast<GamefixId>(enum_cast(id) + 1))
{
if (fix.compare(Pcsx2Config::GamefixOptions::GetGameFixName(id)) == 0 &&
std::find(gameEntry.gameFixes.begin(), gameEntry.gameFixes.end(), id) == gameEntry.gameFixes.end())
{
gameEntry.gameFixes.push_back(id);
fixValidated = true;
break;
}
}
}
if (!fixValidated)
{
Console.Error(fmt::format("GameDB: Invalid gamefix: '{}', specified for serial: '{}'. Dropping!", fix, serial));
}
}
}
if (node.has_child("speedHacks") && node["speedHacks"].has_children())
{
if (is_override)
gameEntry.speedHacks.clear();
for (const auto& n : node["speedHacks"].children())
{
const std::string_view id_view = std::string_view(n.key().str, n.key().len);
const std::string_view value_view = std::string_view(n.val().str, n.val().len);
const std::optional<SpeedHack> id = Pcsx2Config::SpeedhackOptions::ParseSpeedHackName(id_view);
const std::optional<int> value = StringUtil::FromChars<int>(value_view);
if (id.has_value() && value.has_value() &&
std::none_of(gameEntry.speedHacks.begin(), gameEntry.speedHacks.end(),
[&id](const auto& it) { return it.first == id.value(); }))
{
gameEntry.speedHacks.emplace_back(id.value(), value.value());
}
else
{
Console.Error(fmt::format("GameDB: Invalid speedhack: '{}={}', specified for serial: '{}'. Dropping!",
id_view, value_view, serial));
}
}
}
if (node.has_child("gsHWFixes"))
{
if (is_override)
gameEntry.gsHWFixes.clear();
for (const auto& n : node["gsHWFixes"].children())
{
const std::string_view id_name(n.key().data(), n.key().size());
std::optional<GameDatabaseSchema::GSHWFixId> id = GameDatabaseSchema::parseHWFixName(id_name);
std::optional<s32> value;
if (id.has_value() && (id.value() == GameDatabaseSchema::GSHWFixId::GetSkipCount ||
id.value() == GameDatabaseSchema::GSHWFixId::BeforeDraw ||
id.value() == GameDatabaseSchema::GSHWFixId::MoveHandler))
{
const std::string_view str_value(n.has_val() ? std::string_view(n.val().data(), n.val().size()) : std::string_view());
if (id.value() == GameDatabaseSchema::GSHWFixId::GetSkipCount)
value = GSLookupGetSkipCountFunctionId(str_value);
else if (id.value() == GameDatabaseSchema::GSHWFixId::BeforeDraw)
value = GSLookupBeforeDrawFunctionId(str_value);
else if (id.value() == GameDatabaseSchema::GSHWFixId::MoveHandler)
value = GSLookupMoveHandlerFunctionId(str_value);
if (value.value_or(-1) < 0)
{
Console.Error(fmt::format("GameDB: Invalid GS HW Fix Value for '{}' in '{}': '{}'", id_name, serial, str_value));
continue;
}
}
else
{
value = n.has_val() ? StringUtil::FromChars<s32>(std::string_view(n.val().data(), n.val().size())) : 1;
}
if (!id.has_value() || !value.has_value())
{
Console.Error(fmt::format("GameDB: Invalid GS HW Fix: '{}' specified for serial '{}'. Dropping!", id_name, serial));
continue;
}
gameEntry.gsHWFixes.emplace_back(id.value(), value.value());
}
}
// Memory Card Filters - Store as a vector to allow flexibility in the future
// - currently they are used as a '\n' delimited string in the app
if (node.has_child("memcardFilters") && node["memcardFilters"].has_children())
{
if (is_override)
gameEntry.memcardFilters.clear();
for (const auto& n : node["memcardFilters"].children())
{
auto memcardFilter = std::string(n.val().str, n.val().len);
gameEntry.memcardFilters.emplace_back(std::move(memcardFilter));
}
}
// Game Patches
if (node.has_child("patches") && node["patches"].has_children())
{
// Patches are keyed by CRC — clearing on override means upstream's
// CRC-keyed patches are dropped when the override ships its own block.
if (is_override)
gameEntry.patches.clear();
for (const auto& n : node["patches"].children())
{
// use a crc of 0 for default patches
const std::string_view crc_str(n.key().str, n.key().len);
const std::optional<u32> crc = (StringUtil::compareNoCase(crc_str, "default")) ? std::optional<u32>(0) : StringUtil::FromChars<u32>(crc_str, 16);
if (!crc.has_value())
{
Console.Error(fmt::format("GameDB: Invalid CRC '{}' found for serial: '{}'. Skipping!", crc_str, serial));
continue;
}
if (gameEntry.patches.find(crc.value()) != gameEntry.patches.end())
{
Console.Error(fmt::format("GameDB: Duplicate CRC '{}' found for serial: '{}'. Skipping, CRCs are case-insensitive!", crc_str, serial));
continue;
}
std::string patch;
if (n.has_child("content"))
n["content"] >> patch;
gameEntry.patches.emplace(crc.value(), std::move(patch));
}
}
if (node.has_child("dynaPatches") && node["dynaPatches"].has_children())
{
if (is_override)
gameEntry.dynaPatches.clear();
for (const auto& n : node["dynaPatches"].children())
{
Patch::DynamicPatch patch;
if (n.has_child("pattern") && n["pattern"].has_children())
{
for (const auto& db_pattern : n["pattern"].children())
{
Patch::DynamicPatchEntry entry;
db_pattern["offset"] >> entry.offset;
db_pattern["value"] >> entry.value;
patch.pattern.push_back(entry);
}
for (const auto& db_replacement : n["replacement"].children())
{
Patch::DynamicPatchEntry entry;
db_replacement["offset"] >> entry.offset;
db_replacement["value"] >> entry.value;
patch.replacement.push_back(entry);
}
}
gameEntry.dynaPatches.push_back(patch);
}
}
}
void GameDatabase::parseAndInsert(const std::string_view serial, const ryml::NodeRef& node)
{
GameDatabaseSchema::GameEntry gameEntry;
populateEntry(gameEntry, serial, node, /*is_override=*/false);
s_game_db.emplace(std::string(serial), std::move(gameEntry));
}
void GameDatabase::mergeOverride(const std::string_view serial, const ryml::NodeRef& node)
{
// Fetch-or-create: when the serial already exists (a PARTIAL override of
// upstream), the override's scalars replace and its list fields clear-then-
// replace; when it's a NEW serial, this creates it from scratch, same shape
// as a normal entry.
auto& gameEntry = s_game_db[std::string(serial)];
populateEntry(gameEntry, serial, node, /*is_override=*/true);
}
static const char* s_round_modes[static_cast<u32>(FPRoundMode::MaxCount)] = {
"Nearest",
"NegativeInfinity",
"PositiveInfinity",
"Chop"};
static const char* s_gs_hw_fix_names[] = {
"autoFlush",
"cpuFramebufferConversion",
"readTCOnClose",
"disableDepthSupport",
"preloadFrameData",
"disablePartialInvalidation",
"textureInsideRT",
"limit24BitDepth",
"alignSprite",
"mergeSprite",
"mipmap",
"accurateAlphaTest",
"forceEvenSpritePosition",
"bilinearUpscale",
"nativePaletteDraw",
"estimateTextureRegion",
"drawBuffering",
"PCRTCOffsets",
"PCRTCOverscan",
"coalesceRenderPasses",
"trilinearFiltering",
"skipDrawStart",
"skipDrawEnd",
"halfPixelOffset",
"roundSprite",
"nativeScaling",
"texturePreloading",
"deinterlace",
"cpuSpriteRenderBW",
"cpuSpriteRenderLevel",
"cpuCLUTRender",
"gpuTargetCLUT",
"gpuPaletteConversion",
"minimumBlendingLevel",
"maximumBlendingLevel",
"recommendedBlendingLevel",
"recommendedAccurateAlphaTest",
"recommendedHWAA1",
"hwDownloadMode",
"getSkipCount",
"beforeDraw",
"moveHandler",
};
static_assert(std::size(s_gs_hw_fix_names) == static_cast<u32>(GameDatabaseSchema::GSHWFixId::Count), "HW fix name lookup is correct size");
const char* GameDatabaseSchema::getHWFixName(GSHWFixId id)
{
return s_gs_hw_fix_names[static_cast<u32>(id)];
}
static std::optional<GameDatabaseSchema::GSHWFixId> GameDatabaseSchema::parseHWFixName(const std::string_view name)
{
for (u32 i = 0; i < std::size(s_gs_hw_fix_names); i++)
{
if (name.compare(s_gs_hw_fix_names[i]) == 0)
return static_cast<GameDatabaseSchema::GSHWFixId>(i);
}
return std::nullopt;
}
bool GameDatabaseSchema::isUserHackHWFix(GSHWFixId id)
{
switch (id)
{
case GSHWFixId::Deinterlace:
case GSHWFixId::Mipmap:
case GSHWFixId::TexturePreloading:
case GSHWFixId::TrilinearFiltering:
case GSHWFixId::MinimumBlendingLevel:
case GSHWFixId::MaximumBlendingLevel:
case GSHWFixId::RecommendedBlendingLevel:
case GSHWFixId::PCRTCOffsets:
case GSHWFixId::PCRTCOverscan:
case GSHWFixId::GetSkipCount:
case GSHWFixId::BeforeDraw:
case GSHWFixId::MoveHandler:
return false;
default:
return true;
}
}
void GameDatabaseSchema::GameEntry::applyGameFixes(Pcsx2Config& config, bool applyAuto) const
{
// Only apply core game fixes if the user has enabled them.
if (!applyAuto)
Console.Warning("GameDB: Game Fixes are disabled");
if (eeRoundMode < FPRoundMode::MaxCount)
{
if (applyAuto)
{
Console.WriteLn("GameDB: Changing EE/FPU roundmode to %d [%s]", eeRoundMode, s_round_modes[static_cast<u8>(eeRoundMode)]);
config.Cpu.FPUFPCR.SetRoundMode(eeRoundMode);
}
else
{
Console.Warning("GameDB: Skipping changing EE/FPU roundmode to %d [%s]", eeRoundMode, s_round_modes[static_cast<u8>(eeRoundMode)]);
}
}
if (eeDivRoundMode < FPRoundMode::MaxCount)
{
if (applyAuto)
{
Console.WriteLn("GameDB: Changing EE/FPU divison roundmode to %d [%s]", eeRoundMode, s_round_modes[static_cast<u8>(eeDivRoundMode)]);
config.Cpu.FPUDivFPCR.SetRoundMode(eeDivRoundMode);
}
else
{
Console.Warning("GameDB: Skipping changing EE/FPU roundmode to %d [%s]", eeRoundMode, s_round_modes[static_cast<u8>(eeRoundMode)]);
}
}
if (vu0RoundMode < FPRoundMode::MaxCount)
{
if (applyAuto)
{
Console.WriteLn("GameDB: Changing VU0 roundmode to %d [%s]", vu0RoundMode, s_round_modes[static_cast<u8>(vu0RoundMode)]);
config.Cpu.VU0FPCR.SetRoundMode(vu0RoundMode);
}
else
{
Console.Warning("GameDB: Skipping changing VU0 roundmode to %d [%s]", vu0RoundMode, s_round_modes[static_cast<u8>(vu0RoundMode)]);
}
}
if (vu1RoundMode < FPRoundMode::MaxCount)
{
if (applyAuto)
{
Console.WriteLn("GameDB: Changing VU1 roundmode to %d [%s]", vu1RoundMode, s_round_modes[static_cast<u8>(vu1RoundMode)]);
config.Cpu.VU1FPCR.SetRoundMode(vu1RoundMode);
}
else
{
Console.Warning("GameDB: Skipping changing VU1 roundmode to %d [%s]", vu1RoundMode, s_round_modes[static_cast<u8>(vu1RoundMode)]);
}
}
if (eeClampMode != GameDatabaseSchema::ClampMode::Undefined)
{
const int clampMode = enum_cast(eeClampMode);
if (applyAuto)
{
Console.WriteLn("GameDB: Changing EE/FPU clamp mode [mode=%d]", clampMode);
config.Cpu.Recompiler.fpuOverflow = (clampMode >= 1);
config.Cpu.Recompiler.fpuExtraOverflow = (clampMode >= 2);
config.Cpu.Recompiler.fpuFullMode = (clampMode >= 3);
}
else
Console.Warning("GameDB: Skipping changing EE/FPU clamp mode [mode=%d]", clampMode);
}
if (vu0ClampMode != GameDatabaseSchema::ClampMode::Undefined)
{
const int clampMode = enum_cast(vu0ClampMode);
if (applyAuto)
{
Console.WriteLn("GameDB: Changing VU0 clamp mode [mode=%d]", clampMode);
config.Cpu.Recompiler.vu0Overflow = (clampMode >= 1);
config.Cpu.Recompiler.vu0ExtraOverflow = (clampMode >= 2);
config.Cpu.Recompiler.vu0SignOverflow = (clampMode >= 3);
}
else
Console.Warning("GameDB: Skipping changing VU0 clamp mode [mode=%d]", clampMode);
}
if (vu1ClampMode != GameDatabaseSchema::ClampMode::Undefined)
{
const int clampMode = enum_cast(vu1ClampMode);
if (applyAuto)
{
Console.WriteLn("GameDB: Changing VU1 clamp mode [mode=%d]", clampMode);
config.Cpu.Recompiler.vu1Overflow = (clampMode >= 1);
config.Cpu.Recompiler.vu1ExtraOverflow = (clampMode >= 2);
config.Cpu.Recompiler.vu1SignOverflow = (clampMode >= 3);
}
else
Console.Warning("GameDB: Skipping changing VU1 clamp mode [mode=%d]", clampMode);
}
// TODO - config - this could be simplified with maps instead of bitfields and enums
for (const auto& it : speedHacks)
{
if (!applyAuto)
{
Console.Warning("GameDB: Skipping setting Speedhack '%s' to [mode=%d]",
Pcsx2Config::SpeedhackOptions::GetSpeedHackName(it.first), it.second);
continue;
}
// Legacy note - speedhacks are setup in the GameDB as integer values, but
// are effectively booleans like the gamefixes
config.Speedhacks.Set(it.first, it.second);
Console.WriteLn("GameDB: Setting Speedhack '%s' to [mode=%d]",
Pcsx2Config::SpeedhackOptions::GetSpeedHackName(it.first), it.second);
}
// TODO - config - this could be simplified with maps instead of bitfields and enums
for (const GamefixId id : gameFixes)
{
if (!applyAuto)
{
Console.Warning("GameDB: Skipping Gamefix: %s", Pcsx2Config::GamefixOptions::GetGameFixName(id));
continue;
}
// if the fix is present, it is said to be enabled
config.Gamefixes.Set(id, true);
Console.WriteLn("GameDB: Enabled Gamefix: %s", Pcsx2Config::GamefixOptions::GetGameFixName(id));
// The LUT is only used for 1 game so we allocate it only when the gamefix is enabled (save 4MB)
if (id == Fix_GoemonTlbMiss && true)
vtlb_Alloc_Ppmap();
}
}
bool GameDatabaseSchema::GameEntry::configMatchesHWFix(const Pcsx2Config::GSOptions& config, GSHWFixId id, int value)
{
switch (id)
{
case GSHWFixId::AutoFlush:
return (static_cast<int>(config.UserHacks_AutoFlush) == value);
case GSHWFixId::CPUFramebufferConversion:
return (static_cast<int>(config.UserHacks_CPUFBConversion) == value);
case GSHWFixId::FlushTCOnClose:
return (static_cast<int>(config.UserHacks_ReadTCOnClose) == value);
case GSHWFixId::DisableDepthSupport:
return (static_cast<int>(config.UserHacks_DisableDepthSupport) == value);
case GSHWFixId::PreloadFrameData:
return (static_cast<int>(config.PreloadFrameWithGSData) == value);
case GSHWFixId::DisablePartialInvalidation:
return (static_cast<int>(config.UserHacks_DisablePartialInvalidation) == value);
case GSHWFixId::TextureInsideRT:
return (static_cast<int>(config.UserHacks_TextureInsideRt) == value);
case GSHWFixId::Limit24BitDepth:
return (static_cast<int>(config.UserHacks_Limit24BitDepth) == value);
case GSHWFixId::AlignSprite:
return (config.UpscaleMultiplier <= 1.0f || static_cast<int>(config.UserHacks_AlignSpriteX) == value);
case GSHWFixId::MergeSprite:
return (config.UpscaleMultiplier <= 1.0f || static_cast<int>(config.UserHacks_MergePPSprite) == value);
case GSHWFixId::ForceEvenSpritePosition:
return (config.UpscaleMultiplier <= 1.0f || static_cast<int>(config.UserHacks_ForceEvenSpritePosition) == value);
case GSHWFixId::BilinearUpscale:
return (config.UpscaleMultiplier <= 1.0f || static_cast<int>(config.UserHacks_BilinearHack) == value);
case GSHWFixId::NativePaletteDraw:
return (config.UpscaleMultiplier <= 1.0f || static_cast<int>(config.UserHacks_NativePaletteDraw) == value);
case GSHWFixId::EstimateTextureRegion:
return (static_cast<int>(config.UserHacks_EstimateTextureRegion) == value);
case GSHWFixId::DrawBuffering:
return (static_cast<int>(config.UserHacks_DrawBuffering) == value);
case GSHWFixId::PCRTCOffsets:
return (static_cast<int>(config.PCRTCOffsets) == value);
case GSHWFixId::PCRTCOverscan:
return (static_cast<int>(config.PCRTCOverscan) == value);
case GSHWFixId::CoalesceRenderPasses:
return (static_cast<int>(config.CoalesceRenderPasses) == value);
case GSHWFixId::Mipmap:
return (static_cast<int>(config.HWMipmap) == value);
case GSHWFixId::AccurateAlphaTest:
return (static_cast<int>(config.HWAccurateAlphaTest) == value);
case GSHWFixId::TrilinearFiltering:
return (config.TriFilter == TriFiltering::Automatic || static_cast<int>(config.TriFilter) == value);
case GSHWFixId::SkipDrawStart:
return (config.SkipDrawStart == value);
case GSHWFixId::SkipDrawEnd:
return (config.SkipDrawEnd == value);
case GSHWFixId::HalfPixelOffset:
return (config.UpscaleMultiplier <= 1.0f || config.UserHacks_HalfPixelOffset == static_cast<GSHalfPixelOffset>(value));
case GSHWFixId::RoundSprite:
return (config.UpscaleMultiplier <= 1.0f || config.UserHacks_RoundSprite == value);
case GSHWFixId::NativeScaling:
return (config.UpscaleMultiplier <= 1.0f || static_cast<int>(config.UserHacks_NativeScaling) == value);
case GSHWFixId::TexturePreloading:
return (static_cast<int>(config.TexturePreloading) <= value);
case GSHWFixId::Deinterlace:
return (config.InterlaceMode == GSInterlaceMode::Automatic || static_cast<int>(config.InterlaceMode) == value);
case GSHWFixId::HWDownloadMode:
// A non-default user choice already "matches" (we never override it — see the apply switch).
return (config.HWDownloadMode != GSHardwareDownloadMode::Enabled || static_cast<int>(config.HWDownloadMode) == value);
case GSHWFixId::CPUSpriteRenderBW:
return (config.UserHacks_CPUSpriteRenderBW == value);
case GSHWFixId::CPUSpriteRenderLevel:
return (config.UserHacks_CPUSpriteRenderLevel == value);
case GSHWFixId::CPUCLUTRender:
return (config.UserHacks_CPUCLUTRender == value);
case GSHWFixId::GPUTargetCLUT:
return (static_cast<int>(config.UserHacks_GPUTargetCLUTMode) == value);
case GSHWFixId::GPUPaletteConversion:
return (config.GPUPaletteConversion == ((value > 1) ? (config.TexturePreloading == TexturePreloadingLevel::Full) : (value != 0)));
case GSHWFixId::MinimumBlendingLevel:
return (static_cast<int>(config.AccurateBlendingUnit) >= value);
case GSHWFixId::MaximumBlendingLevel:
return (static_cast<int>(config.AccurateBlendingUnit) <= value);
case GSHWFixId::RecommendedBlendingLevel:
return true;
case GSHWFixId::GetSkipCount:
return (static_cast<int>(config.GetSkipCountFunctionId) == value);
case GSHWFixId::BeforeDraw:
return (static_cast<int>(config.BeforeDrawFunctionId) == value);
case GSHWFixId::MoveHandler:
return (static_cast<int>(config.MoveHandlerFunctionId) == value);
default:
return false;
}
}
void GameDatabaseSchema::GameEntry::applyGSHardwareFixes(Pcsx2Config::GSOptions& config) const
{
std::string disabled_fixes;
// Only apply GS HW fixes if the user hasn't manually enabled HW fixes.
const bool apply_auto_fixes = !config.ManualUserHacks;
const bool is_sw_renderer = EmuConfig.GS.Renderer == GSRendererType::SW;
if (!apply_auto_fixes)
Console.Warning("GameDB: Manual GS hardware renderer fixes are enabled, not using automatic hardware renderer fixes from GameDB.");
for (const auto& [id, value] : gsHWFixes)
{
if (isUserHackHWFix(id) && !apply_auto_fixes)
{
if (configMatchesHWFix(config, id, value))
continue;
Console.Warning("GameDB: Skipping GS Hardware Fix: %s to [mode=%d]", getHWFixName(id), value);
fmt::format_to(std::back_inserter(disabled_fixes), "{} {} = {}", disabled_fixes.empty() ? " " : "\n ", getHWFixName(id), value);
continue;
}
switch (id)
{
case GSHWFixId::AutoFlush:
{
if (value >= 0 && value <= static_cast<int>(GSHWAutoFlushLevel::Enabled))
config.UserHacks_AutoFlush = static_cast<GSHWAutoFlushLevel>(value);
}
break;
case GSHWFixId::CPUFramebufferConversion:
config.UserHacks_CPUFBConversion = (value > 0);
break;
case GSHWFixId::FlushTCOnClose:
config.UserHacks_ReadTCOnClose = (value > 0);
break;
case GSHWFixId::DisableDepthSupport:
config.UserHacks_DisableDepthSupport = (value > 0);
break;
case GSHWFixId::PreloadFrameData:
config.PreloadFrameWithGSData = (value > 0);
break;
case GSHWFixId::HWDownloadMode:
// GameDB carries the recommended GS Hardware Download Mode as a DEFAULT only:
// it fills in for games the player hasn't touched, but must never override a
// deliberate choice (their per-game/global Hardware Download Mode wins, since
// LoadCoreSettings runs before this). So only apply when the current value is
// still the default (Enabled). Enable manual HW fixes to force Accurate back.
if (config.HWDownloadMode == GSHardwareDownloadMode::Enabled &&
value > static_cast<int>(GSHardwareDownloadMode::Enabled) &&
value <= static_cast<int>(GSHardwareDownloadMode::Disabled))
{
config.HWDownloadMode = static_cast<GSHardwareDownloadMode>(value);
}
break;
case GSHWFixId::DisablePartialInvalidation:
config.UserHacks_DisablePartialInvalidation = (value > 0);
break;
case GSHWFixId::TextureInsideRT:
{
if (value >= 0 && value <= static_cast<int>(GSTextureInRtMode::MergeTargets))
config.UserHacks_TextureInsideRt = static_cast<GSTextureInRtMode>(value);
}
break;
case GSHWFixId::Limit24BitDepth:
{
if (value >= 0 && value <= static_cast<int>(GSLimit24BitDepth::PrioritizeLower))
config.UserHacks_Limit24BitDepth = static_cast<GSLimit24BitDepth>(value);
}
break;
case GSHWFixId::AlignSprite:
config.UserHacks_AlignSpriteX = (value > 0);
break;
case GSHWFixId::MergeSprite:
config.UserHacks_MergePPSprite = (value > 0);
break;
case GSHWFixId::ForceEvenSpritePosition:
config.UserHacks_ForceEvenSpritePosition = (value > 0);
break;
case GSHWFixId::BilinearUpscale:
{
if (value >= 0 && value < static_cast<int>(GSBilinearDirtyMode::MaxCount))
config.UserHacks_BilinearHack = static_cast<GSBilinearDirtyMode>(value);
}
break;
case GSHWFixId::NativePaletteDraw:
config.UserHacks_NativePaletteDraw = (value > 0);
break;
case GSHWFixId::EstimateTextureRegion:
config.UserHacks_EstimateTextureRegion = (value > 0);
break;
case GSHWFixId::DrawBuffering:
config.UserHacks_DrawBuffering = (value > 0);
break;
case GSHWFixId::PCRTCOffsets:
config.PCRTCOffsets = (value > 0);
break;
case GSHWFixId::PCRTCOverscan:
config.PCRTCOverscan = (value > 0);
break;
case GSHWFixId::CoalesceRenderPasses:
config.CoalesceRenderPasses = (value > 0);
break;
case GSHWFixId::Mipmap:
config.HWMipmap = (value > 0);
break;
case GSHWFixId::AccurateAlphaTest:
config.HWAccurateAlphaTest = (value > 0);
break;
case GSHWFixId::TrilinearFiltering:
{
if (value >= 0 && value <= static_cast<int>(TriFiltering::Forced))
{
if (config.TriFilter == TriFiltering::Automatic)
config.TriFilter = static_cast<TriFiltering>(value);
else if (config.TriFilter > TriFiltering::Off)
Console.Warning("GameDB: Game requires trilinear filtering to be disabled.");
}
}
break;
case GSHWFixId::SkipDrawStart:
config.SkipDrawStart = value;
break;
case GSHWFixId::SkipDrawEnd:
config.SkipDrawEnd = value;
break;
case GSHWFixId::HalfPixelOffset:
{
if (value >= 0 && value < static_cast<int>(GSHalfPixelOffset::MaxCount))
config.UserHacks_HalfPixelOffset = static_cast<GSHalfPixelOffset>(value);
}
break;
case GSHWFixId::RoundSprite:
config.UserHacks_RoundSprite = value;
break;
case GSHWFixId::NativeScaling:
config.UserHacks_NativeScaling = static_cast<GSNativeScaling>(value);
break;
case GSHWFixId::TexturePreloading:
{
if (value >= 0 && value <= static_cast<int>(TexturePreloadingLevel::Full))
config.TexturePreloading = std::min(config.TexturePreloading, static_cast<TexturePreloadingLevel>(value));
}
break;
case GSHWFixId::Deinterlace:
{
if (value >= static_cast<int>(GSInterlaceMode::Automatic) && value < static_cast<int>(GSInterlaceMode::Count))
{
if (config.InterlaceMode == GSInterlaceMode::Automatic)
config.InterlaceMode = static_cast<GSInterlaceMode>(value);
else
Console.Warning("GameDB: Game requires different deinterlace mode but it has been overridden by user setting.");
}
}
break;
case GSHWFixId::CPUSpriteRenderBW:
config.UserHacks_CPUSpriteRenderBW = value;
break;
case GSHWFixId::CPUSpriteRenderLevel:
config.UserHacks_CPUSpriteRenderLevel = value;
break;
case GSHWFixId::CPUCLUTRender:
config.UserHacks_CPUCLUTRender = value;
break;
case GSHWFixId::GPUTargetCLUT:
{
if (value >= 0 && value <= static_cast<int>(GSGPUTargetCLUTMode::InsideTarget))
config.UserHacks_GPUTargetCLUTMode = static_cast<GSGPUTargetCLUTMode>(value);
}
break;
case GSHWFixId::GPUPaletteConversion:
{
// if 2, enable paltex when preloading is full, otherwise leave as-is
if (value > 1)
config.GPUPaletteConversion = (config.TexturePreloading == TexturePreloadingLevel::Full) ? true : config.GPUPaletteConversion;
else
config.GPUPaletteConversion = (value != 0);
}
break;
case GSHWFixId::MinimumBlendingLevel:
{
if (value >= 0 && value <= static_cast<int>(AccBlendLevel::Maximum))
config.AccurateBlendingUnit = std::max(config.AccurateBlendingUnit, static_cast<AccBlendLevel>(value));
}
break;
case GSHWFixId::MaximumBlendingLevel:
{
if (value >= 0 && value <= static_cast<int>(AccBlendLevel::Maximum))
config.AccurateBlendingUnit = std::min(config.AccurateBlendingUnit, static_cast<AccBlendLevel>(value));
}
break;
case GSHWFixId::RecommendedBlendingLevel:
{
if (!is_sw_renderer && value >= 0 && value <= static_cast<int>(AccBlendLevel::Maximum) && static_cast<int>(EmuConfig.GS.AccurateBlendingUnit) < value)
{
static constexpr std::array<const char*, static_cast<u8>(AccBlendLevel::MaxCount)> s_blending_option_names = {{
TRANSLATE_NOOP("GameDatabase", "Minimum"),
TRANSLATE_NOOP("GameDatabase", "Basic"),
TRANSLATE_NOOP("GameDatabase", "Medium"),
TRANSLATE_NOOP("GameDatabase", "High"),
TRANSLATE_NOOP("GameDatabase", "Full"),
TRANSLATE_NOOP("GameDatabase", "Maximum"),
}};
Host::AddKeyedOSDMessage("HWBlendingWarning",
fmt::format(TRANSLATE_FS("GameDatabase",
"{0} Current Blending Accuracy is {1}.\n"
"Recommended Blending Accuracy for this game is {2}.\n"
"You can adjust the blending level in Game Properties to improve\n"
"graphical quality, but this will increase system requirements."),
ICON_FA_PAINTBRUSH,
s_blending_option_names[static_cast<u8>(EmuConfig.GS.AccurateBlendingUnit)],
s_blending_option_names[static_cast<u8>(value)]),
Host::OSD_WARNING_DURATION);
}
else
{
Host::RemoveKeyedOSDMessage("HWBlendingWarning");
}
}
break;
case GSHWFixId::RecommendedAccurateAlphaTest:
{
if (!is_sw_renderer && value >= 0 && value <= 1 &&
static_cast<int>(config.HWAccurateAlphaTest) < value)
{
Host::AddKeyedOSDMessage("HWAlphaTestWarning",
fmt::format(TRANSLATE_FS("GameDatabase",
"{0} Accurate Alpha Test is currently disabled.\n"
"This game recommends enabling Accurate Alpha Test.\n"
"You can enable it in Game Properties to improve graphical\n"
"accuracy, but this may increase system requirements."),
ICON_FA_PAINTBRUSH),
Host::OSD_WARNING_DURATION);
}
else
{
Host::RemoveKeyedOSDMessage("HWAlphaTestWarning");
}
}
break;
case GSHWFixId::RecommendedHWAA1:
{
if (!is_sw_renderer && value >= 0 && value <= 1 &&
static_cast<int>(config.HWAA1) < value)
{
Host::AddKeyedOSDMessage("HWAA1Warning",
fmt::format(TRANSLATE_FS("GameDatabase",
"{0} AA1 is currently disabled.\n"
"This game recommends enabling AA1.\n"
"You can enable it in Game Properties to improve graphical\n"
"accuracy, but this may increase system requirements."),
ICON_FA_PAINTBRUSH),
Host::OSD_WARNING_DURATION);
}
else
{
Host::RemoveKeyedOSDMessage("HWAA1Warning");
}
}
break;
case GSHWFixId::GetSkipCount:
config.GetSkipCountFunctionId = static_cast<s16>(value);
break;
case GSHWFixId::BeforeDraw:
config.BeforeDrawFunctionId = static_cast<s16>(value);
break;
case GSHWFixId::MoveHandler:
config.MoveHandlerFunctionId = static_cast<s16>(value);
break;
default:
break;
}
Console.WriteLn("GameDB: Enabled GS Hardware Fix: %s to [mode=%d]", getHWFixName(id), value);
}
// fixup skipdraw range just in case the db has a bad range (but the linter should catch this)
config.SkipDrawEnd = std::max(config.SkipDrawStart, config.SkipDrawEnd);
if (!is_sw_renderer && !disabled_fixes.empty())
{
Host::AddKeyedOSDMessage("HWFixesWarning",
fmt::format(ICON_FA_WAND_MAGIC_SPARKLES " {}\n{}",
TRANSLATE_SV("GameDatabase", "Manual GS hardware renderer fixes are enabled, automatic fixes were not applied:"),
disabled_fixes),
Host::OSD_ERROR_DURATION);
}
else
{
Host::RemoveKeyedOSDMessage("HWFixesWarning");
}
}
void GameDatabase::loadFile(const std::string& path, const std::string& name, bool is_override)
{
const std::optional<std::string> buffer = FileSystem::ReadFileToString(path.c_str());
if (!buffer.has_value())
{
// Override file is optional — silent skip when not shipped.
// Upstream is mandatory — keep the error loud.
if (!is_override)
Console.Error("GameDB: Unable to open GameDB file, file does not exist.");
return;
}
const ryml::csubstr yaml = ryml::to_csubstr(*buffer);
Error error;
std::optional<ryml::Tree> tree = ParseYAMLFromString(yaml, ryml::to_csubstr(name), &error, true);
if (!tree.has_value())
{
Console.ErrorFmt("GameDB: Failed to parse game database file {}:", path);
Console.Error(error.GetDescription());
return;
}
ryml::NodeRef root = tree->rootref();
for (const ryml::NodeRef& n : root.children())
{
auto serial = StringUtil::toLower(std::string(n.key().str, n.key().len));
// Serials and CRCs must be inserted as lower-case, as that is how they are retrieved
// this is because the application may pass a lowercase CRC or serial along
//
// However, YAML's keys are as expected case-sensitive, so we have to explicitly do our own duplicate checking.
// The override file legitimately reuses upstream serials by design — that's the whole point — so the duplicate
// guard only fires in the upstream load path.
if (!is_override && s_game_db.count(serial) == 1)
{
Console.ErrorFmt("GameDB: Duplicate serial '{}' found in GameDB. Skipping, Serials are case-insensitive!", serial);
continue;
}
if (n.is_map())
{
if (is_override)
mergeOverride(serial, n);
else
parseAndInsert(serial, n);
}
}
}
void GameDatabase::initDatabase()
{
loadFile(
Path::Combine(EmuFolders::Resources, GAMEDB_YAML_FILE_NAME),
GAMEDB_YAML_FILE_NAME,
/*is_override=*/false);
loadFile(
Path::Combine(EmuFolders::Resources, GAMEDB_OVERRIDE_YAML_FILE_NAME),
GAMEDB_OVERRIDE_YAML_FILE_NAME,
/*is_override=*/true);
}
void GameDatabase::ensureLoaded()
{
std::call_once(s_load_once_flag, []() {
Common::Timer timer;
Console.WriteLn(fmt::format("GameDB: Has not been initialized yet, initializing..."));
initDatabase();
Console.WriteLn("GameDB: %zu games on record (loaded in %.2fms)", s_game_db.size(), timer.GetTimeMilliseconds());
});
}
const GameDatabaseSchema::GameEntry* GameDatabase::findGame(const std::string_view serial)
{
GameDatabase::ensureLoaded();
auto iter = s_game_db.find(StringUtil::toLower(serial));
return (iter != s_game_db.end()) ? &iter->second : nullptr;
}
bool GameDatabase::TrackHash::parseHash(const std::string_view str)
{
constexpr u32 expected_length = SIZE * 2;
if (str.length() != expected_length)
return false;
std::memset(data, 0, sizeof(data));
for (u32 i = 0; i < SIZE * 2; i++)
{
const char ch = str[i];
u8 b;
if (ch >= '0' && ch <= '9')
b = static_cast<u8>(ch - '0');
else if (ch >= 'a' && ch <= 'f')
b = static_cast<u8>(ch - 'a') + 0xa;
else if (ch >= 'A' && ch <= 'F')
b = static_cast<u8>(ch - 'A') + 0xa;
else
return false;
data[i / 2] |= ((i % 2) == 0) ? (b << 4) : b;
}
return true;
}
std::string GameDatabase::TrackHash::toString() const
{
return fmt::format(
"{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7],
data[8], data[9], data[10], data[11], data[12], data[13], data[14], data[15]);
}
struct TrackHashHasher
{
size_t operator()(const GameDatabase::TrackHash& hash) const
{
return std::hash<std::string_view>()(std::string_view(reinterpret_cast<const char*>(hash.data),
GameDatabase::TrackHash::SIZE));
}
};
static constexpr char HASHDB_YAML_FILE_NAME[] = "RedumpDatabase.yaml";
std::unordered_map<GameDatabase::TrackHash, u32, TrackHashHasher> s_track_hash_to_entry_map;
std::vector<GameDatabase::HashDatabaseEntry> s_hash_database;
static bool parseHashDatabaseEntry(const ryml::NodeRef& node)
{
if (!node.has_child("name") || !node.has_child("hashes"))
{
Console.Warning("[HashDatabase] Incomplete entry found.");
return false;
}
GameDatabase::HashDatabaseEntry entry;
node["name"] >> entry.name;
if (node.has_child("version"))
node["version"] >> entry.version;
if (node.has_child("serial"))
node["serial"] >> entry.serial;
const u32 index = static_cast<u32>(s_hash_database.size());
for (const ryml::ConstNodeRef& n : node["hashes"].children())
{
if (!n.is_map() || !n.has_child("size") || !n.has_child("md5"))
{
Console.ErrorFmt("[HashDatabase] Incomplete hash definition in {}", entry.name);
return false;
}
GameDatabase::TrackHash th;
std::string md5;
n["md5"] >> md5;
n["size"] >> th.size;
if (!th.parseHash(md5))
{
Console.ErrorFmt("[HashDatabase] Failed to parse hash in {}: '{}'", entry.name, md5);
return false;
}
if (entry.tracks.empty() && s_track_hash_to_entry_map.find(th) != s_track_hash_to_entry_map.end())
Console.WarningFmt("[HashDatabase] Duplicate first track hash in {}", entry.name);
entry.tracks.push_back(th);
s_track_hash_to_entry_map.emplace(th, index);
}
s_hash_database.push_back(std::move(entry));
return true;
}
bool GameDatabase::loadHashDatabase()
{
if (!s_hash_database.empty())
return true;
Common::Timer load_timer;
const std::string path(Path::Combine(EmuFolders::Resources, HASHDB_YAML_FILE_NAME));
const std::string name(HASHDB_YAML_FILE_NAME);
std::optional<std::string> buffer = FileSystem::ReadFileToString(path.c_str());
if (!buffer.has_value())
{
Console.Error("[HashDatabase] Unable to open hash database file, file does not exist.");
return false;
}
ryml::csubstr yaml = ryml::to_csubstr(*buffer);
Error error;
std::optional<ryml::Tree> tree = ParseYAMLFromString(yaml, ryml::to_csubstr(name), &error);
if (!tree.has_value())
{
Console.ErrorFmt("[HashDatabase] Failed to parse hash database file {}:", path);
Console.Error(error.GetDescription());
return false;
}
ryml::NodeRef root = tree->rootref();
bool okay = true;
for (const ryml::NodeRef& n : root.children())
{
if (!parseHashDatabaseEntry(n))
{
okay = false;
break;
}
}
if (!okay)
{
s_track_hash_to_entry_map.clear();
s_hash_database.clear();
return false;
}
Console.WriteLn(Color_StrongGreen, "[HashDatabase] Loaded YAML in %.0f ms", load_timer.GetTimeMilliseconds());
return true;
}
void GameDatabase::unloadHashDatabase()
{
s_track_hash_to_entry_map.clear();
s_hash_database.clear();
}
static size_t getTrackIndex(const GameDatabase::TrackHash* tracks, size_t num_tracks, const GameDatabase::TrackHash& track)
{
for (size_t i = 0; i < num_tracks; i++)
{
if (tracks[i] == track)
return i;
}
return num_tracks;
}
const GameDatabase::HashDatabaseEntry* GameDatabase::lookupHash(
const TrackHash* tracks, size_t num_tracks, bool* tracks_matched, std::string* match_error)
{
loadHashDatabase();
if (num_tracks == 0)
{
*match_error = TRANSLATE_STR("GameDatabase", "No tracks provided.");
std::memset(tracks_matched, 0, sizeof(bool) * num_tracks);
return nullptr;
}
// match the first track, for DVDs this will be all there is anyway
const auto data_iter = s_track_hash_to_entry_map.find(tracks[0]);
if (data_iter == s_track_hash_to_entry_map.end())
{
*match_error = fmt::format(TRANSLATE_FS("GameDatabase", "Hash {} is not in database."), tracks[0].toString());
std::memset(tracks_matched, 0, sizeof(bool) * num_tracks);
return nullptr;
}
// make sure they're not missing the data track
const GameDatabase::HashDatabaseEntry* candidate = &s_hash_database[data_iter->second];
if (getTrackIndex(candidate->tracks.data(), candidate->tracks.size(), tracks[0]) != 0)
{
*match_error = TRANSLATE_STR("GameDatabase", "Data track number does not match data track in database.");
std::memset(tracks_matched, 0, sizeof(bool) * num_tracks);
return nullptr;
}
// first track is okay!
tracks_matched[0] = true;
match_error->clear();
// now check any audio tracks...
bool all_okay = true;
for (size_t track = 1; track < num_tracks; track++)
{
const auto audio_iter = s_track_hash_to_entry_map.find(tracks[track]);
if (audio_iter != s_track_hash_to_entry_map.end())
{
fmt::format_to(std::back_inserter(*match_error),
TRANSLATE_FS("GameDatabase", "Track {0} with hash {1} is not found in database.\n"), track + 1,
tracks[track].toString());
tracks_matched[track] = false;
all_okay = false;
continue;
}
// same game?
if (audio_iter->second != data_iter->second)
{
fmt::format_to(std::back_inserter(*match_error),
TRANSLATE_FS("GameDatabase", "Track {0} with hash {1} is for a different game ({2}).\n"), track + 1,
tracks[track].toString(), s_hash_database[audio_iter->second].name);
tracks_matched[track] = false;
all_okay = false;
continue;
}
// make sure it's the correct track number
if (getTrackIndex(candidate->tracks.data(), candidate->tracks.size(), tracks[track]) != track)
{
fmt::format_to(std::back_inserter(*match_error),
TRANSLATE_FS("GameDatabase", "Track {0} with hash {1} does not match database track.\n"), track + 1,
tracks[track].toString());
tracks_matched[track] = false;
all_okay = false;
continue;
}
tracks_matched[track] = true;
}
if (!match_error->empty() && match_error->back() == '\n')
match_error->pop_back();
return all_okay ? candidate : nullptr;
}