/** ****************************************************************************** * Xenia : Xbox 360 Emulator Research Project * ****************************************************************************** * Copyright 2022 Ben Vanik. All rights reserved. * * Released under the BSD license - see LICENSE in the root for more details. * ****************************************************************************** */ #include "xenia/base/cvar.h" #include "xenia/base/filesystem.h" #include "xenia/base/logging.h" #include "xenia/config.h" #include "xenia/memory.h" #include "xenia/patcher/patch_db.h" DEFINE_bool(apply_patches, true, "Enables custom patching functionality", "General"); namespace xe { namespace patcher { PatchDB::PatchDB(const std::filesystem::path patches_root) { patches_root_ = patches_root; LoadPatches(); } PatchDB::~PatchDB() {} void PatchDB::LoadPatches() { if (!cvars::apply_patches) { return; } const std::filesystem::path patches_directory = patches_root_ / "patches"; const std::vector patch_files = filesystem::ListFiles(patches_directory); for (const xe::filesystem::FileInfo& patch_file : patch_files) { // Skip files that doesn't have only title_id as name and .patch as // extension if (!std::regex_match(path_to_utf8(patch_file.name), patch_filename_regex_)) { XELOGE("PatchDB: Skipped loading file {} due to incorrect filename", patch_file.name); continue; } const PatchFileEntry loaded_title_patches = ReadPatchFile(patch_file.path / patch_file.name); if (loaded_title_patches.title_id != -1) { loaded_patches_.push_back(loaded_title_patches); } } XELOGI("PatchDB: Loaded patches for {} titles", loaded_patches_.size()); } PatchFileEntry PatchDB::ReadPatchFile( const std::filesystem::path& file_path) const { PatchFileEntry patch_file; toml::parse_result patch_toml_fields; try { patch_toml_fields = ParseFile(file_path); } catch (...) { XELOGE("PatchDB: Cannot load patch file: {}", file_path.filename()); patch_file.title_id = -1; return patch_file; }; auto title_name = patch_toml_fields.get_as("title_name"); auto title_id = patch_toml_fields.get_as("title_id"); auto hashes_node = patch_toml_fields.get("hash"); if (!title_name || !title_id || !hashes_node) { XELOGE("PatchDB: Cannot load patch file: {}", file_path.filename()); patch_file.title_id = -1; return patch_file; } patch_file.title_id = strtoul(title_id->get().c_str(), NULL, 16); patch_file.title_name = title_name->get(); ReadHashes(patch_file, hashes_node); auto patch_array = patch_toml_fields.get("patch"); if (!patch_array->is_array()) { return patch_file; } for (const auto& patch_entry : *patch_array->as_array()) { if (!patch_entry.is_table()) { continue; } PatchInfoEntry patch = PatchInfoEntry(); ReadPatchHeader(patch, patch_entry.as_table()); patch_file.patch_info.push_back(patch); } return patch_file; } bool PatchDB::ReadPatchData(std::vector& patch_data, const std::pair data_type, const toml::table* patch_fields) const { auto patch_data_fields = patch_fields->get_as(data_type.first); if (!patch_data_fields) { return true; } for (const auto& patch_data_table : *patch_data_fields) { if (!patch_data_table.is_table()) { continue; } auto table = patch_data_table.as_table(); auto address = static_cast(table->get_as("address")->get()); size_t alloc_size = (size_t)data_type.second.size; switch (data_type.second.type) { case PatchDataType::kBE8: { uint16_t value = static_cast(table->get_as("value")->get()); patch_data.push_back({address, PatchDataValue(alloc_size, value)}); break; } case PatchDataType::kBE16: { uint16_t value = static_cast(table->get_as("value")->get()); patch_data.push_back( {address, PatchDataValue(alloc_size, xe::byte_swap(value))}); break; } case PatchDataType::kBE32: { uint32_t value = static_cast(table->get_as("value")->get()); patch_data.push_back( {address, PatchDataValue(alloc_size, xe::byte_swap(value))}); break; } case PatchDataType::kBE64: { uint64_t value = table->get_as("value")->get(); patch_data.push_back( {address, PatchDataValue(alloc_size, xe::byte_swap(value))}); break; } case PatchDataType::kF64: { const auto value_field = table->get("value"); double value = 0.0; if (value_field->is_floating_point()) { value = value_field->as_floating_point()->get(); } if (value_field->is_integer()) { value = static_cast(value_field->as_integer()->get()); } patch_data.push_back( {address, PatchDataValue(alloc_size, xe::byte_swap(value))}); break; } case PatchDataType::kF32: { const auto value_field = table->get("value"); float value = 0.0f; if (value_field->is_floating_point()) { value = static_cast(value_field->as_floating_point()->get()); } if (value_field->is_integer()) { value = static_cast(value_field->as_integer()->get()); } patch_data.push_back( {address, PatchDataValue(alloc_size, xe::byte_swap(value))}); break; } case PatchDataType::kString: { std::string value = table->get_as("value")->get(); patch_data.push_back({address, PatchDataValue(value)}); break; } case PatchDataType::kU16String: { std::u16string value = xe::to_utf16(table->get_as("value")->get()); patch_data.push_back({address, PatchDataValue(value)}); break; } case PatchDataType::kByteArray: { std::vector data; const std::string value = table->get_as("value")->get(); bool success = string_util::hex_string_to_array(data, value); if (!success) { XELOGW("PatchDB: Cannot convert hex string to byte array! Skipping", address); return false; } patch_data.push_back({address, PatchDataValue(data)}); break; } default: { XELOGW("PatchDB: Unknown patch data type for address {:08X}! Skipping", address); return false; } } } return true; } std::vector PatchDB::GetTitlePatches( const uint32_t title_id, const std::optional hash) { std::vector title_patches; std::copy_if( loaded_patches_.cbegin(), loaded_patches_.cend(), std::back_inserter(title_patches), [=](const PatchFileEntry entry) { bool hash_exist = std::find(entry.hashes.cbegin(), entry.hashes.cend(), hash) != entry.hashes.cend(); return entry.title_id == title_id && hash_exist; }); return title_patches; } void PatchDB::ReadHashes(PatchFileEntry& patch_entry, const toml::node* hashes_node) const { auto add_hash = [&patch_entry](const toml::node* hash_node) { if (!hash_node->is_string()) { return; } const auto string_hash = hash_node->as_string()->get(); if (string_hash.empty()) { return; } patch_entry.hashes.push_back( xe::string_util::from_string(string_hash, true)); }; if (hashes_node->is_value()) { add_hash(hashes_node); } if (hashes_node->is_array()) { for (const auto& hash_entry : *hashes_node->as_array()) { add_hash(&hash_entry); } } } void PatchDB::ReadPatchHeader(PatchInfoEntry& patch_info, const toml::table* patch_fields) const { std::string patch_name = {}; std::string patch_desc = {}; std::string patch_author = {}; bool is_enabled = false; if (patch_fields->contains("name")) { patch_name = patch_fields->get_as("name")->get(); } if (patch_fields->contains("desc")) { patch_desc = patch_fields->get_as("desc")->get(); } if (patch_fields->contains("author")) { patch_author = patch_fields->get_as("author")->get(); } if (patch_fields->contains("is_enabled")) { is_enabled = patch_fields->get_as("is_enabled")->get(); } patch_info.id = 0; // Todo(Gliniak): Implement id for future GUI stuff patch_info.patch_name = patch_name; patch_info.patch_desc = patch_desc; patch_info.patch_author = patch_author; patch_info.is_enabled = is_enabled; // Iterate through all available data sizes for (const auto& patch_data_type : patch_data_types_size_) { bool success = ReadPatchData(patch_info.patch_data, patch_data_type, patch_fields); if (!success) { XELOGE("PatchDB: Cannot read patch {}", patch_name); break; } } } } // namespace patcher } // namespace xe