diff --git a/CMakeLists.txt b/CMakeLists.txt index 69c6e6f115..647a31c77f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -171,6 +171,8 @@ add_library(${PROJECT_NAME} OBJECT src/game_destiny.h src/game_dynrpg.cpp src/game_dynrpg.h + src/game_dynrpg_loader.cpp + src/game_dynrpg_loader.h src/game_enemy.cpp src/game_enemy.h src/game_enemyparty.cpp diff --git a/Makefile.am b/Makefile.am index e95b677527..64eabfc363 100644 --- a/Makefile.am +++ b/Makefile.am @@ -151,6 +151,8 @@ libeasyrpg_player_a_SOURCES = \ src/game_destiny.h \ src/game_dynrpg.cpp \ src/game_dynrpg.h \ + src/game_dynrpg_loader.cpp \ + src/game_dynrpg_loader.h \ src/game_enemy.cpp \ src/game_enemy.h \ src/game_enemyparty.cpp \ diff --git a/src/exe_buildinfo.h b/src/exe_buildinfo.h index fc6aa932f3..a672d5bddc 100644 --- a/src/exe_buildinfo.h +++ b/src/exe_buildinfo.h @@ -230,6 +230,9 @@ namespace EXE::BuildInfo { patch_segment_data chk_segment_data; size_t extract_var_offset; + constexpr PatchDetectionInfo() : + chk_segment_offset(0), chk_segment_data({}), extract_var_offset(0) { + } constexpr PatchDetectionInfo(size_t chk_segment_offset, patch_segment_data chk_segment_data) : chk_segment_offset(chk_segment_offset), chk_segment_data(chk_segment_data), extract_var_offset(0) { } diff --git a/src/exe_constants.h b/src/exe_constants.h index 53f110325c..c63718ce2a 100644 --- a/src/exe_constants.h +++ b/src/exe_constants.h @@ -431,6 +431,36 @@ namespace EXE::Constants { }} } }}; + + inline EXE::Constants::code_address_map const* GetConstantAddressesForBuildInfo(EXE::BuildInfo::EngineType engine_type, EXE::BuildInfo::KnownEngineBuildVersions build_version) { + switch (engine_type) { + case EXE::BuildInfo::EngineType::RPG2000: + { + auto& builds = known_engine_builds_rm2k; + auto it = std::find_if(builds.begin(), builds.end(), [&](const auto& pair) { + return pair.first == build_version; + }); + if (it != builds.end()) { + return &it->second; + } + } + break; + case EXE::BuildInfo::EngineType::RPG2003: + { + auto& builds = known_engine_builds_rm2k3; + auto it = std::find_if(builds.begin(), builds.end(), [&](const auto& pair) { + return pair.first == build_version; + }); + if (it != builds.end()) { + return &it->second; + } + } + break; + default: + break; + } + return nullptr; + } } #endif diff --git a/src/exe_reader.cpp b/src/exe_reader.cpp index ae8a9921ae..4918f23587 100644 --- a/src/exe_reader.cpp +++ b/src/exe_reader.cpp @@ -493,7 +493,7 @@ int EXEReader::FileInfo::GetEngineType(bool& is_maniac_patch) const { return Player::EngineNone; } -std::map EXEReader::GetOverridenGameConstants() { +std::map EXEReader::GetOverriddenGameConstants() { constexpr bool debug_const_extraction = true; std::map game_constants; @@ -597,32 +597,7 @@ std::map EXEReader::GetOverridenGameCons EXE::BuildInfo::kEngineTypes.tag(build_info.engine_type), EXE::BuildInfo::kKnownEngineBuildDescriptions.tag(build_version)); - EXE::Constants::code_address_map const* constant_addresses = nullptr; - - switch (build_info.engine_type) { - case EXE::BuildInfo::EngineType::RPG2000: - { - auto& builds = EXE::Constants::known_engine_builds_rm2k; - auto it = std::find_if(builds.begin(), builds.end(), [&](const auto& pair) { - return pair.first == build_version; - }); - if (it != builds.end()) { - constant_addresses = &it->second; - } - } - break; - case EXE::BuildInfo::EngineType::RPG2003: - { - auto& builds = EXE::Constants::known_engine_builds_rm2k3; - auto it = std::find_if(builds.begin(), builds.end(), [&](const auto& pair) { - return pair.first == build_version; - }); - if (it != builds.end()) { - constant_addresses = &it->second; - } - } - break; - } + auto constant_addresses = EXE::Constants::GetConstantAddressesForBuildInfo(build_info.engine_type, build_version); switch (build_version) { case EXE::BuildInfo::RM2KE_162: diff --git a/src/exe_reader.h b/src/exe_reader.h index a9ec4ff5b4..21058b11b9 100644 --- a/src/exe_reader.h +++ b/src/exe_reader.h @@ -77,7 +77,7 @@ class EXEReader { const FileInfo& GetFileInfo(); - std::map GetOverridenGameConstants(); + std::map GetOverriddenGameConstants(); std::map GetEmbeddedStrings(std::string encoding); diff --git a/src/exe_shared.h b/src/exe_shared.h index 481c08f814..aa3dbb6ab0 100644 --- a/src/exe_shared.h +++ b/src/exe_shared.h @@ -18,6 +18,8 @@ #ifndef EP_EXE_SHARED_H #define EP_EXE_SHARED_H +#include +#include #include namespace EXE::Shared { @@ -152,6 +154,12 @@ namespace EXE::Shared { KnownPatches patch_type; int32_t custom_var_1; }; + + struct EngineCustomization { + std::map constant_overrides; + std::map strings; + std::map runtime_patches; + }; } #endif diff --git a/src/game_dynrpg_loader.cpp b/src/game_dynrpg_loader.cpp new file mode 100644 index 0000000000..1c7fdb1ae1 --- /dev/null +++ b/src/game_dynrpg_loader.cpp @@ -0,0 +1,357 @@ +/* + * This file is part of EasyRPG Player. + * + * EasyRPG Player is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * EasyRPG Player is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with EasyRPG Player. If not, see . + */ + +// Headers +#include "game_dynrpg_loader.h" +#include "output.h" +#include "player.h" +#include "exe_constants.h" +#include "exe_patches.h" + +#include + +namespace { + std::map known_patches; + + template + bool try_read_hex_str(std::string& str, T& out_val) { + auto non_ws_found = std::find_if(str.begin(), str.end(), [](auto p) { return !std::isxdigit(p); }); + if (non_ws_found != str.end()) { + return false; + } + if constexpr (std::is_same::value) { + out_val = std::stol(str, nullptr, 16); + } else { + out_val = std::stoi(str, nullptr, 16); + } + return true; + } + + bool read_ips_offset(Filesystem_Stream::InputStream& is, int& pos, uint32_t& out_addr, uint32_t& out_size) { + is.seekg(pos++, std::ios_base::beg); + out_addr = is.get() << 16; + is.seekg(pos++, std::ios_base::beg); + out_addr |= is.get() << 8; + is.seekg(pos++, std::ios_base::beg); + out_addr |= is.get(); + + if (out_addr == 0x454f46) { //EOF + return false; + } + + is.seekg(pos++, std::ios_base::beg); + out_size = is.get() << 8; + is.seekg(pos++, std::ios_base::beg); + out_size |= is.get(); + + return true; + } + + bool read_int32(Filesystem_Stream::InputStream& is, int pos) { + int v; + is.seekg(pos++, std::ios_base::beg); + v = is.get(); + is.seekg(pos++, std::ios_base::beg); + v |= is.get() << 8; + is.seekg(pos++, std::ios_base::beg); + v |= is.get() << 8; + is.seekg(pos++, std::ios_base::beg); + v |= is.get() << 8; + return v; + } +} + +std::vector DynRpg_Loader::DetectRuntimePatches() { + auto dir_contents = FileFinder::Game().ListDirectory(DYNRPG_FOLDER_PATCHES); + + if (!dir_contents) { + return {}; + } + + known_patches.clear(); + // DynRPG should always be Rm2k3 v1.08 + for (auto p : EXE::Patches::patches_RM2K3_1080) { + known_patches[p.first] = p.second; + } + + std::vector result; + + for (auto item : *dir_contents) { + auto fis = FileFinder::Game().OpenFile(DYNRPG_FOLDER_PATCHES, item.first); + if (!fis) { + continue; + } + bool is_ips = true; + int i = 0; + for (i = 0; i < magic_ips.size(); i++) { + fis.seekg(i, std::ios_base::beg); + if (fis.get() != magic_ips[i]) { + is_ips = false; + break; + } + } + if (!is_ips) { + continue; + } + + auto patch = ReadIPS(item.first, fis); + if (patch.patch_type != invalid_patch.patch_type) { + result.push_back(patch); + } + } + + return result; +} + +EXE::Shared::PatchSetupInfo DynRpg_Loader::ReadIPS(std::string const& item_name, Filesystem_Stream::InputStream& is) { + int i = magic_ips.size(); + + EXE::Shared::KnownPatches patch_type; + EXE::BuildInfo::PatchDetectionInfo const* patch_info = nullptr; + do { + uint32_t address = 0, size = 0; + if (!read_ips_offset(is, i, address, size)) { + break; + } + auto patch_it = std::find_if(known_patches.begin(), known_patches.end(), [&](auto& p) { + auto& ofs = p.second.chk_segment_offset; + return ofs >= address && ofs < (address + size); + }); + if (patch_it != known_patches.end()) { + patch_type = patch_it->first; + patch_info = &patch_it->second; + } + i += size; + } while (is.get() != -1); + + if (patch_info == nullptr) { + Output::Warning("DynRPG Loader: Encountered unknown IPS patch '{}'", item_name); + return invalid_patch; + } + + if (patch_info->extract_var_offset == 0) { + Output::Debug("DynRPG Loader: Applying patch '{}'", EXE::Shared::kKnownPatches.tag(patch_type)); + return EXE::Shared::PatchSetupInfo { patch_type, 0 }; + } + + i = magic_ips.size(); + int var_id = 0; + do { + uint32_t address = 0, size = 0; + if (!read_ips_offset(is, i, address, size)) { + break; + } + + if (patch_info->extract_var_offset >= address && patch_info->extract_var_offset <= (address + size)) { + int section_ofs = patch_info->extract_var_offset - address; + var_id = read_int32(is, i + section_ofs); + } + + auto patch_it = std::find_if(known_patches.begin(), known_patches.end(), [&](auto& p) { + auto& ofs = p.second.extract_var_offset; + return ofs >= address && ofs < (address + size); + }); + if (patch_it != known_patches.end()) { + patch_info = &patch_it->second; + } + i += size; + } while (is.get() != -1); + + if (var_id == 0) { + Output::Debug("DynRPG Loader: Could not extract variable values for known IPS patch '{}'", item_name); + return invalid_patch; + } + + Output::Debug("DynRPG Loader: Applying patch '{}' with var value '{}'", EXE::Shared::kKnownPatches.tag(patch_type), var_id); + + return EXE::Shared::PatchSetupInfo { patch_type, var_id }; +} + +void DynRpg_Loader::ApplyQuickPatches(EXE::Shared::EngineCustomization& engine_customization) { + std::unique_ptr ini; + + ini = std::make_unique(ToString(DYNRPG_INI_NAME)); + if (ini == nullptr || ini->ParseError() == -1) { + return; + } + + auto section_data = ini->GetSection(DYNRPG_INI_SECTION_QUICKPATCHES); + + if (section_data.size() > 0) { + Output::Debug("Found section for QuickPatches inside DynRPG.ini."); + } + + // DynRPG should always be Rm2k3 v1.08 + auto build_version = EXE::BuildInfo::RM2K3_1080_1080; + + auto const& build_info = EXE::BuildInfo::known_engine_builds[build_version].second; + auto constant_addresses = EXE::Constants::GetConstantAddressesForBuildInfo(build_info.engine_type, build_version); + + std::vector loaded_ips_patches; + + for (auto info : engine_customization.runtime_patches) { + auto it = std::find_if(known_patches.begin(), known_patches.end(), [&](auto& p) { + return p.first == info.first; + }); + if (it == known_patches.end()) { + continue; + } + loaded_ips_patches.push_back({ info.first, it->second }); + } + + bool read_error = false;; + for (auto pair : section_data) { + auto& key_name = pair.first; + auto& addresses_str = pair.second; + + auto qp = ParseQuickPatch(addresses_str, build_info.code_size); + + if (qp.size() == 0) { + Output::Warning("DynRPG Loader: Could not parse QuickPatch: '{}'", key_name); + continue; + } + + std::map quickpatched_constants; + for (auto section : qp) { + auto const_it = std::find_if(constant_addresses->begin(), constant_addresses->end(), [§ion](auto& p) { + return p.second.code_offset == section.address; + }); + if (const_it == constant_addresses->end()) { + read_error = true; + break; + } + quickpatched_constants[const_it->first] = section.patch_val; + } + + if (!read_error) { + Output::Debug("DynRPG Loader: Found valid QuickPatch '{}' targeting known game constants", key_name); + for (auto const_info : quickpatched_constants) { + Output::Debug("DynRPG QuickPatch: Applying value '{}' for constant '{}'", const_info.second, EXE::Shared::kGameConstantType.tag(static_cast(const_info.first))); + engine_customization.constant_overrides[const_info.first] = const_info.second; + } + continue; + } + read_error = false; + + EXE::Shared::KnownPatches quickpatched_ips_type; + int quickpatched_ips_var_id = 0; + for (auto section : qp) { + auto const_it = std::find_if(loaded_ips_patches.begin(), loaded_ips_patches.end(), [§ion](auto& p) { + auto& ofs = p.second.extract_var_offset; + return ofs == section.address; + }); + if (const_it == loaded_ips_patches.end()) { + read_error = true; + break; + } + quickpatched_ips_type = const_it->first; + quickpatched_ips_var_id = section.patch_val; + } + + if (!read_error) { + Output::Debug("DynRPG Loader: Found valid QuickPatch '{}' targeting known patch constants for '{}'", key_name, EXE::Shared::kKnownPatches.tag(quickpatched_ips_type)); + for (auto& p : engine_customization.runtime_patches) { + auto& patch = p.second; + if (patch.patch_type != quickpatched_ips_type) { + continue; + } + patch.custom_var_1 = quickpatched_ips_var_id; + } + continue; + } + + Output::Warning("DynRPG Loader: Encountered unkknown QuickPatch: '{}'", key_name); + } +} + +DynRpg_Loader::DynRpgQuickPatch DynRpg_Loader::ParseQuickPatch(std::string const& line, int code_size) { + DynRpgQuickPatch patched_sections; + + auto skip_to_next = [](std::string const& str, int& i) { + while (i < str.size() && (str[i] != ',' || std::isspace(str[i]))) { + i++; + } + }; + auto skip_whitespace = [](std::string const& str, int& i) { + while (i < str.size() && std::isspace(str[i])) { + i++; + } + }; + + int str_start; + int i = 0; + + do { + skip_whitespace(line, i); + str_start = i; + skip_to_next(line, i); + + if (str_start == i) { + return {}; + } + + auto addr_str = line.substr(str_start, i - str_start); + QuickPatchAddress qp; + + if (!try_read_hex_str(addr_str, qp.address)) { + return {}; + } + + if (qp.address < 0x400 || (qp.address >= 0x400000 && qp.address <= 0x400C00)) { + // HEADER section + return {}; + } + if ((qp.address > (0x400 + code_size) && qp.address < 0x400000) || (qp.address >= (0x400000 + code_size))) { + // Either DATA section or outside of bounds + return {}; + } + if (qp.address > 0x400C00) { + // Convert virtual address to file offset + qp.address -= 0x400C00; + } + + skip_whitespace(line, ++i); + str_start = i; + skip_to_next(line, i); + + if (str_start == i) { + return {}; + } + + auto patch_str = line.substr(str_start, i - str_start); + if (patch_str[0] == '%' || patch_str[0] == '#') { + qp.byte_count = patch_str[0] == '#' ? 4 : 1; + qp.patch_val = atoi(patch_str.c_str() + 1); + } else { + if (!try_read_hex_str(patch_str, qp.patch_val)) { + return {}; + } + qp.byte_count = patch_str.size(); + if ((qp.byte_count % 2) == 1) + qp.byte_count++; + qp.byte_count /= 2; + } + if (qp.patch_val == 0 || qp.byte_count == 0) { + return {}; + } + patched_sections.push_back(qp); + + skip_whitespace(line, ++i); + } while (i < line.size()); + + return patched_sections; +} diff --git a/src/game_dynrpg_loader.h b/src/game_dynrpg_loader.h new file mode 100644 index 0000000000..e04f2e4b00 --- /dev/null +++ b/src/game_dynrpg_loader.h @@ -0,0 +1,54 @@ +/* + * This file is part of EasyRPG Player. + * + * EasyRPG Player is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * EasyRPG Player is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with EasyRPG Player. If not, see . + */ + +#ifndef EP_GAME_DYNRPG_LOADER_H +#define EP_GAME_DYNRPG_LOADER_H + +#include +#include +#include "lcf/inireader.h" +#include "exe_shared.h" +#include "filefinder.h" + +#define DYNRPG_INI_NAME "DynRPG.ini" +#define DYNRPG_INI_SECTION_QUICKPATCHES "QuickPatches" + +#define DYNRPG_FOLDER_PATCHES "DynPatches" +#define DYNRPG_FOLDER_PLUGINS "DynPlugins" + +namespace DynRpg_Loader { + constexpr std::array magic_ips = { 0x50, 0x41, 0x54, 0x43, 0x48 }; // "PATCH" + constexpr EXE::Shared::PatchSetupInfo invalid_patch = { static_cast(-1), 0 }; + + std::vector DetectRuntimePatches(); + + EXE::Shared::PatchSetupInfo ReadIPS(std::string const& item_name, Filesystem_Stream::InputStream& is); + + void ApplyQuickPatches(EXE::Shared::EngineCustomization& engine_customization); + + struct QuickPatchAddress { + uint32_t address = 0; + uint8_t byte_count = 0; + long patch_val = 0; + }; + + using DynRpgQuickPatch = std::vector; + + DynRpgQuickPatch ParseQuickPatch(std::string const& line, int code_size); +} + +#endif diff --git a/src/player.cpp b/src/player.cpp index 1a9bf9498d..1b7f539852 100644 --- a/src/player.cpp +++ b/src/player.cpp @@ -37,6 +37,7 @@ #include "rand.h" #include "cmdline_parser.h" #include "game_dynrpg.h" +#include "game_dynrpg_loader.h" #include "filefinder.h" #include "filefinder_rtp.h" #include "fileext_guesser.h" @@ -765,13 +766,12 @@ void Player::CreateGameObjects() { Cache::exfont_custom = Utils::ReadStream(exfont_stream); } + Constants::ResetOverrides(); DetectEngine(false); Main_Data::filefinder_rtp = std::make_unique(no_rtp_flag, no_rtp_warning_flag, rtp_path); game_config.PrintActivePatches(); - - Constants::ResetOverrides(); Constants::PrintActiveOverrides(); ResetGameObjects(); @@ -789,8 +789,8 @@ void Player::CreateGameObjects() { void Player::DetectEngine(bool ignore_patch_override) { int& engine = game_config.engine; - std::map game_constant_overrides; - std::vector patches; + + EXE::Shared::EngineCustomization engine_customization; #ifndef EMSCRIPTEN // Attempt reading ExFont and version information from RPG_RT.exe (not supported on Emscripten) @@ -817,13 +817,19 @@ void Player::DetectEngine(bool ignore_patch_override) { } } - game_constant_overrides = exe_reader->GetOverridenGameConstants(); + for (auto p : exe_reader->GetOverriddenGameConstants()) { + engine_customization.constant_overrides[p.first] = p.second; + } if (!game_config.patch_override || ignore_patch_override) { - patches = exe_reader->CheckForPatches(); + for (auto p : exe_reader->CheckForPatches()) { + engine_customization.runtime_patches[p.patch_type] = p; + } } - exe_reader->GetEmbeddedStrings(encoding); + for (auto p : exe_reader->GetEmbeddedStrings(encoding)) { + engine_customization.strings[p.first] = p.second; + } if (engine == EngineNone) { Output::Debug("Unable to detect version from exe"); @@ -872,11 +878,20 @@ void Player::DetectEngine(bool ignore_patch_override) { game_config.patch_destiny.Set(true); } +#ifndef EMSCRIPTEN + if (game_config.patch_dynrpg.Get()) { + for (auto p : DynRpg_Loader::DetectRuntimePatches()) { + engine_customization.runtime_patches[p.patch_type] = p; + } + DynRpg_Loader::ApplyQuickPatches(engine_customization); + } +#endif + using Patch = EXE::Patches::KnownPatches; - if (patches.size() > 0) { - for (auto it = patches.begin(); it != patches.end(); ++it) { - auto& patch = *it; + if (engine_customization.runtime_patches.size() > 0) { + for (auto it = engine_customization.runtime_patches.begin(); it != engine_customization.runtime_patches.end(); ++it) { + auto& patch = it->second; switch (static_cast(patch.patch_type)) { case Patch::UnlockPics: @@ -897,7 +912,7 @@ void Player::DetectEngine(bool ignore_patch_override) { break; case Patch::BetterAEP: game_config.new_game.Set(true); - // FIXME: implement BetterAEP�s extended patch functionality + // FIXME: implement BetterAEP's extended patch functionality // -> patch_var (default: 3350) break; case Patch::PicPointer: @@ -914,8 +929,8 @@ void Player::DetectEngine(bool ignore_patch_override) { } } - if (game_constant_overrides.size() > 0) { - for (auto it = game_constant_overrides.begin(); it != game_constant_overrides.end(); ++it) { + if (engine_customization.constant_overrides.size() > 0) { + for (auto it = engine_customization.constant_overrides.begin(); it != engine_customization.constant_overrides.end(); ++it) { Constants::OverrideGameConstant(it->first, it->second); } }