diff --git a/CMakeLists.txt b/CMakeLists.txt index 87859f18f0..647a31c77f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -109,6 +109,11 @@ add_library(${PROJECT_NAME} OBJECT src/dynrpg_textplugin.h src/enemyai.cpp src/enemyai.h + src/exe_buildinfo.h + src/exe_constants.h + src/exe_patches.h + src/exe_shared.h + src/exe_strings.h src/exe_reader.cpp src/exe_reader.h src/exfont.h @@ -166,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 02992d0f91..64eabfc363 100644 --- a/Makefile.am +++ b/Makefile.am @@ -89,6 +89,11 @@ libeasyrpg_player_a_SOURCES = \ src/dynrpg_textplugin.cpp \ src/enemyai.cpp \ src/enemyai.h \ + src/exe_buildinfo.h \ + src/exe_constants.h \ + src/exe_patches.h \ + src/exe_shared.h \ + src/exe_strings.h \ src/exe_reader.cpp \ src/exe_reader.h \ src/exfont.h \ @@ -146,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/battle_message.cpp b/src/battle_message.cpp index 1b89b53f85..0994cbcbac 100644 --- a/src/battle_message.cpp +++ b/src/battle_message.cpp @@ -150,6 +150,15 @@ static std::string GetHpSpRecoveredMessage(const Game_Battler& target, int value Utils::MakeSvArray(target.GetName(), std::to_string(value), points) ); } + + StringView template_text; + if (Player::Constants::HasEmbeddedTemplateString(EXE::Shared::EmbeddedStringTypes::Battle_HpSpRecovery, template_text)) { + return Utils::ReplacePlaceholders( + template_text, + Utils::MakeArray('S', 'U', 'V', 'T'), + Utils::MakeSvArray(target.GetName(), points, std::to_string(value), lcf::Data::terms.hp_recovery) + ); + } std::stringstream ss; std::string particle, particle2, space = ""; @@ -188,6 +197,20 @@ std::string GetParameterAbsorbedMessage(const Game_Battler& source, const Game_B Utils::MakeSvArray(source.GetName(), target.GetName(), std::to_string(value), points) ); } + + auto embedded_str_type = target_is_ally + ? EXE::Shared::EmbeddedStringTypes::Battle_AbsorbAlly + : EXE::Shared::EmbeddedStringTypes::Battle_AbsorbEnemy; + + StringView template_text; + if (Player::Constants::HasEmbeddedTemplateString(embedded_str_type, template_text)) { + return Utils::ReplacePlaceholders( + template_text, + Utils::MakeArray('O', 'U', 'V', 'T'), + Utils::MakeSvArray(target.GetName(), points, std::to_string(value), message) + ); + } + std::stringstream ss; std::string particle, particle2, space = ""; @@ -243,6 +266,20 @@ std::string GetDamagedMessage(const Game_Battler& target, int value) { Utils::MakeSvArray(target.GetName(), std::to_string(value), lcf::Data::terms.health_points) ); } + + auto embedded_str_type = target_is_ally + ? EXE::Shared::EmbeddedStringTypes::Battle_DamageToAlly + : EXE::Shared::EmbeddedStringTypes::Battle_DamageToEnemy; + + StringView template_text; + if (Player::Constants::HasEmbeddedTemplateString(embedded_str_type, template_text)) { + return Utils::ReplacePlaceholders( + template_text, + Utils::MakeArray('S', 'V', 'T'), + Utils::MakeSvArray(target.GetName(), std::to_string(value), message) + ); + } + std::stringstream ss; std::string particle, space = ""; ss << target.GetName(); @@ -276,6 +313,20 @@ std::string GetParameterChangeMessage(const Game_Battler& target, int value, Str Utils::MakeSvArray(target.GetName(), std::to_string(value), points) ); } + + auto embedded_str_type = is_positive + ? EXE::Shared::EmbeddedStringTypes::Battle_StatIncrease + : EXE::Shared::EmbeddedStringTypes::Battle_StatDecrease; + + StringView template_text; + if (Player::Constants::HasEmbeddedTemplateString(embedded_str_type, template_text)) { + return Utils::ReplacePlaceholders( + template_text, + Utils::MakeArray('S', 'U', 'V', 'T'), + Utils::MakeSvArray(target.GetName(), points, std::to_string(value), message) + ); + } + std::stringstream ss; std::string particle, particle2, space = ""; ss << target.GetName(); @@ -437,6 +488,16 @@ std::string GetItemStartMessage2k(const Game_Battler& source, const lcf::rpg::It Utils::MakeSvArray(source.GetName(), item.name) ); } + + //StringView template_text; + //if (Player::Constants::HasEmbeddedTemplateString(EXE::Shared::EmbeddedStringTypes::Battle_UseItem, template_text)) { + // return Utils::ReplacePlaceholders( + // template_text, + // Utils::MakeArray('S', 'O', 'T'), + // Utils::MakeSvArray(source.GetName(), item.name, ToString(lcf::Data::terms.use_item)) + // ); + //} + std::string particle; if (Player::IsCP932()) particle = "は"; diff --git a/src/exe_buildinfo.h b/src/exe_buildinfo.h new file mode 100644 index 0000000000..68b15fa59c --- /dev/null +++ b/src/exe_buildinfo.h @@ -0,0 +1,258 @@ +/* + * 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_EXE_BUILDINFO_H +#define EP_EXE_BUILDINFO_H + +#include +#include +#include "utils.h" + +namespace EXE::BuildInfo { + + enum class EngineType { + UnknownEngine = 0, + RPG2000, + RPG2003, + RM2k3_MP, + EasyRPG + }; + + static constexpr auto kEngineTypes = lcf::makeEnumTags( + "Unknown", + "RPG2000", + "RPG2003", + "RM2k3_MP", + "EasyRPG" + ); + + enum KnownEngineBuildVersions { + UnknownBuild = 0, + + RM2K_20000306, + RM2K_2000XXXX_UNK, + RM2K_20000507, + RM2K_20000619, + RM2K_20000711, + RM2K_20001113, + RM2K_20001115, + RM2K_20001227, + RM2K_20010505, + RM2K_20030327, + RM2K_20030625, + RM2KE_160, + RM2KE_161, + RM2KE_162, + + RM2K3_100, + RM2K3_UNK_1, + RM2K3_UNK_2, + RM2K3_1021_1021, + RM2K3_1030_1030_1, + RM2K3_1030_1030_2, + RM2K3_1030_1040, + RM2K3_1050_1050_1, + RM2K3_1050_1050_2, + RM2K3_1060_1060, + RM2K3_1070_1070, + RM2K3_1080_1080, + RM2K3_1091_1091, + + LAST_RM2K = RM2KE_162, + LAST_RM2K3 = RM2K3_1091_1091, + LAST + }; + + static constexpr auto kKnownEngineBuildDescriptions = lcf::makeEnumTags( + "UnknownBuild", + + "1.00", + "2000-Unk", + "2000-05-07", + "2000-06-19", + "2000-07-11", + "2000-11-13", + "2000-11-15", + "1.07", + "1.10", + "1.50 (VALUE!)", + "1.51 (VALUE!)", + "1.60 (English)", + "1.61 (English)", + "1.62 (English)", + + "1.0.0", + "1.0.1_UNK1", + "1.0.1_UNK2", + "1.0.2.1", + "1.0.3.1", + "1.0.3.2", + "1.0.4.0", + "1.0.5.1", + "1.0.5.2", + "1.0.6", + "1.0.7", + "1.0.8", + "1.0.9.1" + ); + + static_assert(kKnownEngineBuildDescriptions.size() == static_cast(KnownEngineBuildVersions::LAST)); + + constexpr size_t count_known_engine_builds = KnownEngineBuildVersions::LAST; + constexpr size_t count_known_rm2k_builds = 1 + KnownEngineBuildVersions::LAST_RM2K - KnownEngineBuildVersions::RM2K_20000306; + constexpr size_t count_known_rm2k3_builds = 1 + KnownEngineBuildVersions::LAST_RM2K3 - KnownEngineBuildVersions::RM2K3_100; + + struct EngineBuildInfo { + EngineType engine_type; + int32_t code_size; + int32_t entrypoint; + + constexpr EngineBuildInfo() : + engine_type(EngineType::UnknownEngine), code_size(0), entrypoint(0) { + } + + constexpr EngineBuildInfo(EngineType engine_type, int32_t code_size, int32_t entrypoint) : + engine_type(engine_type), code_size(code_size), entrypoint(entrypoint) { + } + }; + + using engine_buildinfo_map = std::array, count_known_engine_builds>; + + constexpr engine_buildinfo_map known_engine_builds = {{ + { UnknownBuild, { EngineType::UnknownEngine, 0, 0 } }, + + { RM2K_20000306, { EngineType::RPG2000, 0x96400, 0x097220 } }, + { RM2K_2000XXXX_UNK, { EngineType::RPG2000, 0x96600, 0x0972C4 } }, + { RM2K_20000507, { EngineType::RPG2000, 0x96600, 0x0972D8 } }, + { RM2K_20000619, { EngineType::RPG2000, 0x96C00, 0x097940 } }, + { RM2K_20000711, { EngineType::RPG2000, 0x96E00, 0x097B50 } }, + { RM2K_20001113, { EngineType::RPG2000, 0x96800, 0x0975E4 } }, + { RM2K_20001115, { EngineType::RPG2000, 0x96800, 0x0975EC } }, + { RM2K_20001227, { EngineType::RPG2000, 0x96A00, 0x097744 } }, + { RM2K_20010505, { EngineType::RPG2000, 0x96A00, 0x097710 } }, + { RM2K_20030327, { EngineType::RPG2000, 0x9BC00, 0x09CA80 } }, + { RM2K_20030625, { EngineType::RPG2000, 0x9BE00, 0x09CC30 } }, + { RM2KE_160, { EngineType::RPG2000, 0x9C000, 0x09CC78 } }, + { RM2KE_161, { EngineType::RPG2000, 0x9CA00, 0x09D708 } }, + { RM2KE_162, { EngineType::RPG2000, 0x9CC00, 0x09D930 } }, + + { RM2K3_100, { EngineType::RPG2003, 0xBD600, 0x0BE3F4 } }, + { RM2K3_UNK_1, { EngineType::RPG2003, 0xBF200, 0x0BFF50 } }, + { RM2K3_UNK_2, { EngineType::RPG2003, 0xBE800, 0x0BF6A8 } }, + { RM2K3_1021_1021, { EngineType::RPG2003, 0xBF400, 0x0C00CC } }, + { RM2K3_1030_1030_1, { EngineType::RPG2003, 0xBFE00, 0x0C0B90 } }, + { RM2K3_1030_1030_2, { EngineType::RPG2003, 0xC0A00, 0x0C1878 } }, + { RM2K3_1030_1040, { EngineType::RPG2003, 0xC0800, 0x0C14D8 } }, + { RM2K3_1050_1050_1, { EngineType::RPG2003, 0xC7400, 0x0C81C4 } }, + { RM2K3_1050_1050_2, { EngineType::RPG2003, 0xC7400, 0x0C81F0 } }, + { RM2K3_1060_1060, { EngineType::RPG2003, 0xC8A00, 0x0C9804 } }, + { RM2K3_1070_1070, { EngineType::RPG2003, 0xC8C00, 0x0C9984 } }, + { RM2K3_1080_1080, { EngineType::RPG2003, 0xC8E00, 0x0C9C1C } }, + { RM2K3_1091_1091, { EngineType::RPG2003, 0xC9000, 0x0C9D24 } } + }}; + + template + class fixed_size_byte_array { + public: + template + constexpr fixed_size_byte_array(Args... args) + : _size(sizeof...(args)), _data(init_fixedsize_array({ args... })) { + } + constexpr size_t size() const { return _size; } + + constexpr const uint8_t operator[](const size_t off) const noexcept { + return _data[off]; + } + private: + template + static constexpr std::array init_fixedsize_array(const std::array input) { + std::array result{}; + for (size_t i = 0; i < N; ++i) { + result[i] = input[i]; + } + return result; + } + + size_t _size; + std::array _data; + }; + + constexpr size_t MAX_SIZE_CHK_PRE = 4; + + using validate_const_data = fixed_size_byte_array; + + class CodeAddressInfo { + public: + int32_t default_val; + uint8_t size_val; + size_t code_offset; + validate_const_data pre_data; + + template + constexpr CodeAddressInfo(int32_t default_val, uint8_t size_val, size_t code_offset, Args... args) : + default_val(default_val), size_val(size_val), code_offset(code_offset), pre_data(validate_const_data{ static_cast(args)... }) { + } + }; + + class CodeAddressStringInfo { + public: + size_t code_offset; + uint32_t crc_jp, crc_en; + validate_const_data pre_data; + + template + constexpr CodeAddressStringInfo(size_t code_offset, uint32_t crc_jp, uint32_t crc_en, Args... args) : + code_offset(code_offset), crc_jp(crc_jp), crc_en(crc_en), pre_data(validate_const_data{ static_cast(args)... }) { + } + }; + + constexpr size_t MAX_SIZE_CHK_PATCH_SEGMENT = 8; + + using patch_segment_data = fixed_size_byte_array; + + struct PatchArg { + size_t offset; + int32_t default_value; + + constexpr PatchArg(size_t offset, int32_t default_value) + : offset(offset), default_value(default_value) { + } + }; + + constexpr PatchArg patch_arg_empty = { 0xFFFFFF, 0 }; + + class PatchDetectionInfo { + public: + size_t chk_segment_offset; + patch_segment_data chk_segment_data; + const PatchArg* patch_args; + size_t patch_args_size; + + constexpr PatchDetectionInfo() : + chk_segment_offset(0), chk_segment_data({}), patch_args(&patch_arg_empty), patch_args_size(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), patch_args(&patch_arg_empty), patch_args_size(0) { + } + template + constexpr PatchDetectionInfo(size_t chk_segment_offset, patch_segment_data chk_segment_data, std::array const& patch_args) : + chk_segment_offset(chk_segment_offset), chk_segment_data(chk_segment_data), patch_args(patch_args.data()), patch_args_size(S) { + } + }; +} + +#endif diff --git a/src/exe_constants.h b/src/exe_constants.h new file mode 100644 index 0000000000..acd99afdc9 --- /dev/null +++ b/src/exe_constants.h @@ -0,0 +1,466 @@ +/* + * 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_EXE_CONSTANTS_H +#define EP_EXE_CONSTANTS_H + +#include "exe_buildinfo.h" +#include "exe_shared.h" + +namespace EXE::Constants { + using namespace BuildInfo; + using GameConstantType = EXE::Shared::GameConstantType; + + enum class KnownPatchConfigurations { + Rm2k3_Italian_WD_108, // Italian "WhiteDragon" patch + StatDelimiter, + LAST + }; + + static constexpr auto kKnownPatchConfigurations = lcf::makeEnumTags( + "Rm2k3 Italian 1.08", + "StatDelimiter Patch" + ); + + static_assert(kKnownPatchConfigurations.size() == static_cast(KnownPatchConfigurations::LAST)); + + using patch_config = std::map; + using T = GameConstantType; + + const std::map known_patch_configurations = { + { + KnownPatchConfigurations::Rm2k3_Italian_WD_108, { + { T::MinVarLimit, -999999999 }, + { T::MaxVarLimit, 999999999 }, + { T::MaxEnemyHP, 999999999 }, + { T::MaxEnemySP, 999999999 }, + { T::MaxActorHP, 99999 }, + { T::MaxActorSP, 9999 }, + { T::MaxAtkBaseValue, 9999 }, + { T::MaxDefBaseValue, 9999 }, + { T::MaxSpiBaseValue, 9999 }, + { T::MaxAgiBaseValue, 9999 }, + { T::MaxDamageValue, 99999 }, + { T::MaxGoldValue, 9999999 } + }},{ + KnownPatchConfigurations::StatDelimiter, { + { T::MaxActorHP, 9999999 }, + { T::MaxActorSP, 9999999 }, + { T::MaxAtkBaseValue, 999999 }, + { T::MaxDefBaseValue, 999999 }, + { T::MaxSpiBaseValue, 999999 }, + { T::MaxAgiBaseValue, 999999 }, + { T::MaxAtkBattleValue, 999999 }, + { T::MaxDefBattleValue, 999999 }, + { T::MaxSpiBattleValue, 999999 }, + { T::MaxAgiBattleValue, 999999 } + }} + }; + + using code_address = std::pair; + using code_address_map = std::array(GameConstantType::LAST)>; + +#define ADD_EAX_ESI 0x03, 0xC6 +#define ADD_EDX_ESI 0x03, 0xD6 +#define MOV_EAX 0xB8 +#define MOV_ECX 0xB9 +#define MOV_EDX 0xBA +#define SUB_EDX_EBX 0x2B, 0xD3 +#define CMP_DWORD_ESP 0x81, 0x7C, 0x24 +#define CMP_ESI 0x81, 0xFE +#define CMP_EAX_BYTE 0x83, 0xF8 +#define CMP_EBX_BYTE 0x83, 0xFB + +//#define DEPENDS_ON_PREVIOUS { 0xFF, 0xFF, 0x00, 0xFF } +// +// constexpr auto magic_prev = std::array(DEPENDS_ON_PREVIOUS); + + template + constexpr code_address map(T default_val, size_t code_offset, Args&&... args) { + return { C, CodeAddressInfo(default_val, sizeof(T), code_offset, std::forward(args)...) }; + } + + template + constexpr code_address not_def() { + return { C, CodeAddressInfo(0, 0, 0) }; + } + + using engine_code_adresses_rm2k = std::array, static_cast(count_known_rm2k_builds)>; + using engine_code_adresses_rm2k3 = std::array, static_cast(count_known_rm2k3_builds)>; + + constexpr code_address_map empty_code_map = {{ + not_def(), + not_def(), + not_def(), + not_def(), + not_def(), + not_def(), + + not_def(), + not_def(), + not_def(), + not_def(), + + not_def(), + not_def(), + not_def(), + not_def(), + + not_def(), + not_def(), + not_def(), + not_def(), + + not_def(), + not_def(), + not_def(), + not_def(), + not_def(), + not_def() + }}; + + constexpr engine_code_adresses_rm2k known_engine_builds_rm2k = {{ + { + RM2K_20000306, + empty_code_map + }, { + RM2K_2000XXXX_UNK, + {{ + map ( -999999, 0x085A0C, CMP_DWORD_ESP, 0x10), + map ( 999999, 0x085A36, CMP_DWORD_ESP, 0x10), + + map ( 160, 0x06D039, MOV_EDX), + map ( 148, 0x06D040, SUB_EDX_EBX, MOV_ECX), + map ( 160, 0x06D05B, MOV_EDX), + map ( 88, 0x06D062, SUB_EDX_EBX, MOV_ECX), + + not_def(), + not_def(), + not_def(), + not_def(), + + not_def(), + not_def(), + not_def(), + not_def(), + + not_def(), + not_def(), + not_def(), + not_def(), + + not_def(), + not_def(), + not_def(), + not_def(), + not_def(), + not_def() + }} + }, { + RM2K_20000507, + empty_code_map + },{ + RM2K_20000619, + empty_code_map + },{ + RM2K_20000711, + {{ + map ( -999999, 0x0846A8, CMP_DWORD_ESP, 0x10), + map ( 999999, 0x0846D2, CMP_DWORD_ESP, 0x10), + + map ( 160, 0x06E491, MOV_EDX), + map ( 148, 0x06E498, SUB_EDX_EBX, MOV_ECX), + map ( 160, 0x06E4B3, MOV_EDX), + map ( 88, 0x06E4BA, SUB_EDX_EBX, MOV_ECX), + + not_def(), + not_def(), + not_def(), + not_def(), + + not_def(), + not_def(), + not_def(), + not_def(), + + not_def(), + not_def(), + not_def(), + not_def(), + + not_def(), + not_def(), + not_def(), + not_def(), + not_def(), + not_def() + }} + },{ + RM2K_20001113, + empty_code_map + },{ + RM2K_20001115, + empty_code_map + },{ + RM2K_20001227, + {{ + map ( -999999, 0x085D78, CMP_DWORD_ESP, 0x10), + map ( 999999, 0x085DA2, CMP_DWORD_ESP, 0x10), + + map ( 160, 0x06D5B9, MOV_EDX), + map ( 148, 0x06D5C0, SUB_EDX_EBX, MOV_ECX), + map ( 160, 0x06D5DB, MOV_EDX), + map ( 88, 0x06D5E2, SUB_EDX_EBX, MOV_ECX), + + not_def(), + not_def(), + not_def(), + not_def(), + + not_def(), + not_def(), + not_def(), + not_def(), + + not_def(), + not_def(), + not_def(), + not_def(), + + not_def(), + not_def(), + not_def(), + not_def(), + not_def(), + not_def() + }} + },{ + RM2K_20010505, + empty_code_map + },{ + RM2K_20030327, + empty_code_map + },{ + RM2K_20030625, + empty_code_map + },{ + RM2KE_160, + empty_code_map + },{ + RM2KE_161, + empty_code_map + },{ + RM2KE_162, + empty_code_map + } + }}; + + constexpr engine_code_adresses_rm2k3 known_engine_builds_rm2k3 = {{ + { + RM2K3_100, + empty_code_map + }, { + RM2K3_UNK_1, + empty_code_map + },{ + RM2K3_UNK_2, + empty_code_map + }, { + RM2K3_1021_1021, + empty_code_map + },{ + RM2K3_1030_1030_1, + empty_code_map + }, { + RM2K3_1030_1030_2, + empty_code_map + },{ + RM2K3_1030_1040, + {{ + map (-9999999, 0x0A60B3, CMP_DWORD_ESP, 0x10), + map ( 9999999, 0x0A60DD, CMP_DWORD_ESP, 0x10), + + map ( 160, 0x08AC49, MOV_EDX), + map ( 148, 0x08AC50, SUB_EDX_EBX, MOV_ECX), + map ( 160, 0x08AC6B, MOV_EDX), + map ( 88, 0x08AC72, SUB_EDX_EBX, MOV_ECX), + + not_def(), + not_def(), + not_def(), + not_def(), + + not_def(), + not_def(), + not_def(), + not_def(), + + not_def(), + not_def(), + not_def(), + not_def(), + + not_def(), + not_def(), + not_def(), + not_def(), + not_def(), + not_def() + }} + }, { + RM2K3_1050_1050_1, + empty_code_map + },{ + RM2K3_1050_1050_2, + empty_code_map + }, { + RM2K3_1060_1060, + {{ + map (-9999999, 0x0AC4F7, CMP_DWORD_ESP, 0x10), + map ( 9999999, 0x0AC521, CMP_DWORD_ESP, 0x10), + + map ( 160, 0x08FB6D, MOV_EDX), + map ( 148, 0x08FB74, SUB_EDX_EBX, MOV_ECX), + map ( 160, 0x08FB8F, MOV_EDX), + map ( 88, 0x08FB96, SUB_EDX_EBX, MOV_ECX), + + not_def(), + not_def(), + not_def(), + not_def(), + + not_def(), + not_def(), + not_def(), + not_def(), + + not_def(), + not_def(), + not_def(), + not_def(), + + not_def(), + not_def(), + not_def(), + not_def(), + not_def(), + not_def() + }} + },{ + RM2K3_1070_1070, + empty_code_map + },{ + RM2K3_1080_1080, + {{ + map (-9999999, 0x0AC76B, CMP_DWORD_ESP, 0x10), + map ( 9999999, 0x0AC795, CMP_DWORD_ESP, 0x10), + + map ( 160, 0x08FC21, MOV_EDX), + map ( 148, 0x08FC28, SUB_EDX_EBX, MOV_ECX), + map ( 160, 0x08FC43, MOV_EDX), + map ( 88, 0x08FC4A, SUB_EDX_EBX, MOV_ECX), + + map ( 9999, 0x0B652B, MOV_ECX), /* 0x0B8590 - 0x0B858B */ + map ( 999, 0x0B659D, MOV_ECX), /* 0x0B85B2 - 0x0B85AD */ + not_def(), + not_def(), + + map ( 999, 0x0B6636, MOV_ECX), /* 0x0B85D1 - 0xB85CC */ + map ( 999, 0x0B689C, MOV_ECX), /* 0x0B85F0 - 0xB85EB */ + map ( 999, 0x0B694C, MOV_ECX), /* 0x0B860F - 0xB860A */ + map ( 999, 0x0B69F2, MOV_ECX), /* 0x0B862E - 0xB8629 */ + + map ( 9999, 0x0BEF3C, MOV_ECX), + map ( 9999, 0x0BF008, MOV_ECX), + map ( 9999, 0x0BF0D1, MOV_ECX), + map ( 9999, 0x0BF16D, MOV_ECX), + + map ( 9999, 0x09C43C, MOV_EAX), + map ( 9999999, 0x0B60C3, CMP_ESI), + map ( 999999, 0x0A5B54, ADD_EDX_ESI, MOV_EAX), + map ( 99, 0x092399, CMP_EAX_BYTE), + map ( 16, 0x08FB34, CMP_EBX_BYTE), + not_def(), + }} + },{ + RM2K3_1091_1091, + {{ + map (-9999999, 0x0B5103, CMP_DWORD_ESP, 0x10), + map ( 9999999, 0x0B512D, CMP_DWORD_ESP, 0x10), + + map ( 160, 0x08EE15, MOV_EDX), + map ( 148, 0x08EE1C, SUB_EDX_EBX, MOV_ECX), + map ( 160, 0x08EE37, MOV_EDX), + map ( 88, 0x08EE3E, SUB_EDX_EBX, MOV_ECX), + + map ( 9999, 0x0AD543, MOV_ECX), + map ( 999, 0x0AD5B5, MOV_ECX), + not_def(), + not_def(), + + map ( 999, 0x0AD64E, MOV_ECX), + map ( 999, 0x0AD8B4, MOV_ECX), + map ( 999, 0x0AD964, MOV_ECX), + map ( 999, 0x0ADA0A, MOV_ECX), + + map ( 9999, 0x0A92B8, MOV_ECX), + map ( 9999, 0x0A9384, MOV_ECX), + map ( 9999, 0x0A944D, MOV_ECX), + map ( 9999, 0x0A94E9, MOV_ECX), + + map ( 9999, 0x09B770, MOV_EAX), + map ( 9999999, 0x0AD0DB, CMP_ESI), + map ( 999999, 0x0A3EDC, ADD_EDX_ESI, MOV_EAX), + map ( 99, 0x09158D, CMP_EAX_BYTE), + map ( 16, 0x08ED28, CMP_EBX_BYTE), + not_def(), + }} + } + }}; + + inline 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 empty_code_map; + } +} + +#endif diff --git a/src/exe_patches.h b/src/exe_patches.h new file mode 100644 index 0000000000..bdbc13347f --- /dev/null +++ b/src/exe_patches.h @@ -0,0 +1,213 @@ +/* + * 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_EXE_PATCHES_H +#define EP_EXE_PATCHES_H + +#include "exe_buildinfo.h" +#include "exe_shared.h" +#include "span.h" + +namespace EXE::Patches { + using namespace BuildInfo; + using KnownPatches = EXE::Shared::KnownPatches; + + template + constexpr patch_segment_data patch_segment(Args... args) { + return patch_segment_data{ static_cast(args)... }; + } + + using patch_detection = std::pair; + template + using patch_detection_map = std::array; + + template + using patch_addresses = std::array const; + + namespace BetterAEP { + constexpr patch_addresses<1> addr_rm2k_107 = {{ { 0x096CA4, 3350 } }}; + constexpr patch_addresses<1> addr_rm2k3_1080 = {{ { 0x0C91A4, 3350 } }}; + } + + namespace DirectMenu { + constexpr patch_addresses<1> addr_rm2k_107 = {{ { 0x0321B5, 3326 } }}; + constexpr patch_addresses<1> addr_rm2k_110 = {{ { 0x032145, 3326 } }}; + constexpr patch_addresses<1> addr_rm2k_150 = {{ { 0x032241, 3326 } }}; + constexpr patch_addresses<1> addr_rm2k_151 = {{ { 0x032285, 3326 } }}; + constexpr patch_addresses<1> addr_rm2k_162 = {{ { 0x0327E9, 3326 } }}; + constexpr patch_addresses<1> addr_rm2k3_1080 = {{ { 0x00462DE, 3326 } }}; + constexpr patch_addresses<1> addr_rm2k3_1090 = {{ { 0x00462DE, 3326 } }}; + } + + namespace MonSca { + constexpr patch_addresses<11> addr_rm2k_107 = {{ + { 0x7E947, 1001 }, // MaxHP modifier -> V[x] + { 0x7E952, 1002 }, // MaxSP modifier -> V[x] + { 0x7E95D, 1003 }, // ATk modifier -> V[x] + { 0x7E968, 1004 }, // Def modifier -> V[x] + { 0x7E973, 1005 }, // Spi modifier -> V[x] + { 0x7E97E, 1006 }, // Agi modifier -> V[x] + { 0x7E989, 1007 }, // EXP modifier -> V[x] + { 0x7E994, 1008 }, // Gold modifier -> V[x] + { 0x7E99F, 1009 }, // Item-ID modifier -> V[x] + { 0x7E9AC, 1010 }, // Item-Drop modifier -> V[x] + { 0x7ED2D, 1001 } // Use alternate formula -> Sw[x] + }}; + constexpr patch_addresses<11> addr_rm2k3_1080 = {{ + { 0xBCB76, 1001 }, + { 0xBCB81, 1002 }, + { 0xBCB8C, 1003 }, + { 0xBCB97, 1004 }, + { 0xBCBA2, 1005 }, + { 0xBCBAD, 1006 }, + { 0xBCBB8, 1007 }, + { 0xBCBC3, 1008 }, + { 0xBCBCE, 1009 }, + { 0xBCBDB, 1010 }, + { 0xBD10B, 1001 } + }}; + } + + namespace MonScaPlus { + constexpr patch_addresses<11> addr_rm2k3_1080 = {{ + { 0xBCB76, 1000 }, + { 0xBCB81, 1008 }, + { 0xBCB8C, 1016 }, + { 0xBCB97, 1024 }, + { 0xBCBA2, 1032 }, + { 0xBCBAD, 1040 }, + { 0xBCBB8, 1048 }, + { 0xBCBC3, 1056 }, + { 0xBCBCE, 1064 }, + { 0xBCBDB, 1072 }, + { 0xBD117, 1001 } + }}; + } + + namespace EXPlus { + // EXP modifier for Party Actors -> V[x] - V[x+3] + constexpr patch_addresses<1> addr_rm2k_107 = {{ { 0x073B92, 3333 } }}; + constexpr patch_addresses<1> addr_rm2k3_1080 = {{ { 0x0990D2, 3333 } }}; + } + + namespace EXPlusPlus { + // Party position of last actor checked via "IsActorInParty" -> V[x] + constexpr patch_addresses<1> addr_rm2k_107 = {{ { 0x088974, 3332 } }}; + constexpr patch_addresses<1> addr_rm2k3_1080 = {{ { 0x0AFAB8, 3332 } }}; + } + + constexpr patch_detection_map<11> patches_RM2K_107 = { { + { KnownPatches::UnlockPics, { 0x082B4D, patch_segment(0x90, 0x90, 0x90, 0x90, 0x90) } }, + { KnownPatches::CommonThisEvent, { 0x084F4C, patch_segment(0xE9, 0xAF, 0xBC, 0xFA, 0xFF) } }, + { KnownPatches::BreakLoopFix, { 0x08A62A, patch_segment(0xE8, 0x85, 0xE8, 0xFF, 0xFF) } }, + { KnownPatches::AutoEnterPatch, { 0x06D420, patch_segment(0x75, 0x75) } }, + { KnownPatches::BetterAEP, { 0x06D75F, patch_segment(0xEB, 0x07), BetterAEP::addr_rm2k_107 } }, + { KnownPatches::PicPointer, { 0x087BE6, patch_segment(0xE8, 0xE9, 0xBF, 0xF9, 0xFF) } }, + { KnownPatches::PicPointer_R, { 0x087BE6, patch_segment(0xE8, 0x51, 0xC0, 0xF9, 0xFF) } }, + { KnownPatches::DirectMenu, { 0x078FC8, patch_segment(0xE9, 0x13, 0x92, 0xFB, 0xFF), DirectMenu::addr_rm2k_107 } }, + { KnownPatches::MonSca, { 0x07E880, patch_segment(0x50, 0x8B, 0x40, 0x14, 0x89), MonSca::addr_rm2k_107 } }, + { KnownPatches::EXPlus, { 0x07394D, patch_segment(0x50, 0xEB, 0x09), EXPlus::addr_rm2k_107 } }, + { KnownPatches::EXPlusPlus, { 0x088BD5, patch_segment(0xE9, 0x91, 0xFD), EXPlusPlus::addr_rm2k_107 } } + }}; + + constexpr patch_detection_map<4> patches_RM2K_110 = {{ + { KnownPatches::CommonThisEvent, { 0x084E5C, patch_segment(0xE9, 0x33, 0xBD, 0xFA, 0xFF) } }, + { KnownPatches::BreakLoopFix, { 0x08A73A, patch_segment(0xE8, 0x85, 0xE8, 0xFF, 0xFF) } }, + { KnownPatches::PicPointer_R, { 0x023B28, patch_segment(0xE8, 0xD5, 0xC0, 0xF9, 0xFF) } }, + { KnownPatches::DirectMenu, { 0x078ED8, patch_segment(0xE9, 0x93, 0x92, 0xFB, 0xFF), DirectMenu::addr_rm2k_110 } } + }}; + + constexpr patch_detection_map<3> patches_RM2K_150 = {{ + { KnownPatches::CommonThisEvent, { 0x089EAC, patch_segment(0xE9, 0xDF, 0x6D, 0xFA, 0xFF) } }, + { KnownPatches::BreakLoopFix, { 0x08F72A, patch_segment(0xE8, 0x8D, 0xE8, 0xFF, 0xFF) } }, + { KnownPatches::DirectMenu, { 0x07DE30, patch_segment(0xE9, 0x37, 0x44, 0xFB, 0xFF), DirectMenu::addr_rm2k_150 } } + }}; + + constexpr patch_detection_map<4> patches_RM2K_151 = {{ + { KnownPatches::CommonThisEvent, { 0x089FC8, patch_segment(0xE9, 0x07, 0x6D, 0xFA, 0xFF) } }, + { KnownPatches::BreakLoopFix, { 0x08F846, patch_segment(0xE8, 0x8D, 0xE8, 0xFF, 0xFF) } }, + { KnownPatches::PicPointer_R, { 0x08CD82, patch_segment(0xE8, 0x89, 0x6F, 0xF9, 0xFF) } }, + { KnownPatches::DirectMenu, { 0x07DF4C, patch_segment(0xE9, 0x5F, 0x43, 0xFB, 0xFF), DirectMenu::addr_rm2k_151 } } + }}; + + constexpr patch_detection_map<2> patches_RM2K_160 = {{ + { KnownPatches::CommonThisEvent, { 0x088838, patch_segment(0xE9, 0xEB, 0x85, 0xFA, 0xFF) } }, + { KnownPatches::BreakLoopFix, { 0x08E098, patch_segment(0xE8, 0xAF, 0xE8, 0xFF, 0xFF) } } + }}; + + constexpr patch_detection_map<2> patches_RM2K_161 = {{ + { KnownPatches::CommonThisEvent, { 0x089208, patch_segment(0xE9, 0x2B, 0x80, 0xFA, 0xFF) } }, + { KnownPatches::BreakLoopFix, { 0x08EAA4, patch_segment(0xE8, 0xAF, 0xE8, 0xFE, 0xFF) } } + }}; + + constexpr patch_detection_map<4> patches_RM2K_162 = {{ + { KnownPatches::CommonThisEvent, { 0x08B8D0, patch_segment(0xE9, 0x63, 0x59, 0xFA, 0xFF) } }, + { KnownPatches::BreakLoopFix, { 0x09116C, patch_segment(0xE8, 0xAF, 0xE8, 0xFF, 0xFF) } }, + { KnownPatches::PicPointer_R, { 0x08E6CA, patch_segment(0xE8, 0xA5, 0x5B, 0xF9, 0xFF) } }, + { KnownPatches::DirectMenu, { 0x07F848, patch_segment(0xE9, 0xC7, 0x2F, 0xFB, 0xFF), DirectMenu::addr_rm2k_162 } } + }}; + + constexpr patch_detection_map<12> patches_RM2K3_1080 = {{ + { KnownPatches::UnlockPics, { 0x0B12FA, patch_segment(0x90, 0x90, 0x90, 0x90, 0x90) } }, + { KnownPatches::CommonThisEvent, { 0x0AB670, patch_segment(0xE9, 0x8B, 0x76, 0xF9, 0xFF) } }, + { KnownPatches::BreakLoopFix, { 0x0B1E4B, patch_segment(0xE8, 0x04, 0xE2, 0xFF, 0xFF) } }, + { KnownPatches::AutoEnterPatch, { 0x08FAC0, patch_segment(0x75, 0x3D) } }, + { KnownPatches::BetterAEP, { 0x08FDA7, patch_segment(0xEB, 0x07), BetterAEP::addr_rm2k3_1080 } }, + { KnownPatches::PicPointer, { 0x0AEB1E, patch_segment(0xE8, 0x01, 0x5F, 0xF8, 0xFF) } }, + { KnownPatches::PicPointer_R, { 0x0AEB1E, patch_segment(0xE8, 0x65, 0x5F, 0xF8, 0xFF) } }, + { KnownPatches::DirectMenu, { 0x0A0422, patch_segment(0xE9, 0xE2, 0x5E, 0xFA, 0xFF), DirectMenu::addr_rm2k3_1080 } }, + { KnownPatches::MonSca, { 0x0BCA31, patch_segment(0x50, 0x8B, 0x40, 0x14, 0x89), MonSca::addr_rm2k3_1080 } }, + { KnownPatches::MonScaPlus, { 0x0BD109, patch_segment(0x5A, 0x0F, 0xAF, 0xC2, 0x99), MonScaPlus::addr_rm2k3_1080 } }, + { KnownPatches::EXPlus, { 0x098E8D, patch_segment(0x50, 0xEB, 0x09), EXPlus::addr_rm2k3_1080 } }, + { KnownPatches::EXPlusPlus, { 0x0AFD19, patch_segment(0xE9, 0x91, 0xFD), EXPlusPlus::addr_rm2k3_1080 } } + }}; + + constexpr patch_detection_map<4> patches_RM2K3_1091 = {{ + { KnownPatches::CommonThisEvent, { 0x0B4008, patch_segment(0xE9, 0xF3, 0xEC, 0xF8, 0xFF) } }, + { KnownPatches::BreakLoopFix, { 0x0BA7E3, patch_segment(0xE8, 0x04, 0xE2, 0xFF, 0xFF) } }, + { KnownPatches::PicPointer_R, { 0x0B74B6, patch_segment(0xE8, 0xCD, 0xD5, 0xF7, 0xFF) } }, + { KnownPatches::DirectMenu, { 0x09F756, patch_segment(0xE9, 0xAE, 0x6B, 0xFA, 0xFF), DirectMenu::addr_rm2k3_1090 } } + }}; + + inline Span GetPatchesForBuildVersion(EXE::BuildInfo::KnownEngineBuildVersions build_version) { + switch (build_version) { + case EXE::BuildInfo::RM2K_20001227: + return EXE::Patches::patches_RM2K_107; + case EXE::BuildInfo::RM2K_20010505: + return EXE::Patches::patches_RM2K_110; + case EXE::BuildInfo::RM2K_20030327: + return EXE::Patches::patches_RM2K_150; + case EXE::BuildInfo::RM2K_20030625: + return EXE::Patches::patches_RM2K_151; + case EXE::BuildInfo::RM2KE_160: + return EXE::Patches::patches_RM2K_160; + case EXE::BuildInfo::RM2KE_161: + return EXE::Patches::patches_RM2K_161; + case EXE::BuildInfo::RM2KE_162: + return EXE::Patches::patches_RM2K_162; + case EXE::BuildInfo::RM2K3_1080_1080: + return EXE::Patches::patches_RM2K3_1080; + case EXE::BuildInfo::RM2K3_1091_1091: + return EXE::Patches::patches_RM2K3_1091; + default: + break; + } + return Span(); + } +} + +#endif diff --git a/src/exe_reader.cpp b/src/exe_reader.cpp index 4785f021a8..d87e6f363f 100644 --- a/src/exe_reader.cpp +++ b/src/exe_reader.cpp @@ -25,6 +25,7 @@ #include #include #include +#include namespace { // hashes of known RPG_RT startup logos @@ -74,6 +75,8 @@ EXEReader::EXEReader(Filesystem_Stream::InputStream core) : corefile(std::move(c uint32_t sectionsOfs = optional_header + GetU16(ofs + 0x14); // skip opt header uint32_t data_directory_ofs = (format_pe32 ? 0x60 : 0x70); resource_rva = GetU32(optional_header + data_directory_ofs + 16); + file_info.entrypoint = format_pe32 ? GetU32(optional_header + 0x10) : 0; + if (!resource_rva) { // Is some kind of encrypted EXE -> Give up return; @@ -90,6 +93,7 @@ EXEReader::EXEReader(Filesystem_Stream::InputStream core) : corefile(std::move(c if (secName == 0x45444F43) { // CODE file_info.code_size = sectVs; + file_info.code_ofs = GetU32(sectionsOfs + 0x14); } else if (secName == 0x52454843) { // CHER(RY) file_info.cherry_size = sectVs; } else if (secName == 0x50454547) { // GEEP @@ -406,7 +410,7 @@ bool EXEReader::ResNameCheck(uint32_t i, const char* p) { } void EXEReader::FileInfo::Print() const { - Output::Debug("RPG_RT information: version={} logos={} code={:#x} cherry={:#x} geep={:#x} arch={} easyrpg={}", version_str, logos, code_size, cherry_size, geep_size, kMachineTypes[machine_type], is_easyrpg_player); + Output::Debug("RPG_RT information: version={} logos={} code={:#x} entrypoint={:#x} cherry={:#x} geep={:#x} arch={} easyrpg={}", version_str, logos, code_size, entrypoint, cherry_size, geep_size, kMachineTypes[machine_type], is_easyrpg_player); } int EXEReader::FileInfo::GetEngineType(bool& is_maniac_patch) const { @@ -489,4 +493,292 @@ int EXEReader::FileInfo::GetEngineType(bool& is_maniac_patch) const { return Player::EngineNone; } +EXE::BuildInfo::KnownEngineBuildVersions EXEReader::GetEngineBuildVersion() { + for (auto it = EXE::BuildInfo::known_engine_builds.begin(); it != EXE::BuildInfo::known_engine_builds.end(); ++it) { + auto& curr_build_info = std::get(*it); + + if (file_info.code_size != curr_build_info.code_size || file_info.entrypoint != curr_build_info.entrypoint) { + continue; + } + build_version = std::get(*it); + build_info = curr_build_info; + break; + } + + return build_version; +} + +std::map EXEReader::GetOverriddenGameConstants() { + constexpr bool debug_const_extraction = true; + + std::map game_constants; + int code_offset = file_info.code_ofs - 0x400; + + auto match_surrounding_data = [&](const EXE::BuildInfo::CodeAddressInfo& info, const uint32_t const_ofs) { + for (int i = 0; i < info.pre_data.size(); i++) { + if (info.pre_data[i] != GetU8(const_ofs - info.pre_data.size() + i)) + return false; + } + /*for (int i = 0; i < info.post_data.size(); i++) { + if (info.post_data[i] != GetU8(const_ofs + sizeof(uint32_t) + i)) + return false; + }*/ + //Is a hit -> constant value can be extracted + return true; + }; + + auto apply_known_config = [&](EXE::Constants::KnownPatchConfigurations conf) { + Output::Debug("Assuming known patch config '{}'", EXE::Constants::kKnownPatchConfigurations.tag(static_cast(conf))); + auto it_conf = EXE::Constants::known_patch_configurations.find(conf); + assert(it_conf != EXE::Constants::known_patch_configurations.end()); + + for (auto it = it_conf->second.begin(); it != it_conf->second.end(); ++it) { + game_constants[it->first] = it->second; + } + }; + + if (build_version == EXE::BuildInfo::KnownEngineBuildVersions::UnknownBuild) { + GetEngineBuildVersion(); + } + + if (build_version != EXE::BuildInfo::KnownEngineBuildVersions::UnknownBuild) { + Output::Debug("Assuming {} build '{}' for constant extraction", + EXE::BuildInfo::kEngineTypes.tag(build_info.engine_type), + EXE::BuildInfo::kKnownEngineBuildDescriptions.tag(build_version)); + + auto& constant_addresses = EXE::Constants::GetConstantAddressesForBuildInfo(build_info.engine_type, build_version); + + switch (build_version) { + case EXE::BuildInfo::RM2KE_162: + if (CheckForString(0x07DEA6, "XXX" /* 3x "POP EAX" */)) { + apply_known_config(EXE::Constants::KnownPatchConfigurations::StatDelimiter); + } + break; + case EXE::BuildInfo::RM2K3_1080_1080: + if (CheckForString(0x08EFE0, "NoTitolo")) { + apply_known_config(EXE::Constants::KnownPatchConfigurations::Rm2k3_Italian_WD_108); + } + if (CheckForString(0x09D679, "XXX" /* 3x "POP EAX" */)) { + apply_known_config(EXE::Constants::KnownPatchConfigurations::StatDelimiter); + } + break; + case EXE::BuildInfo::RM2K3_1091_1091: + if (CheckForString(0x09C9AD, "XXX" /* 3x "POP EAX" */)) { + apply_known_config(EXE::Constants::KnownPatchConfigurations::StatDelimiter); + } + break; + default: + break; + } + + uint32_t const_ofs; + bool extract_success = false; + + for (auto it = constant_addresses.begin(); it != constant_addresses.end(); ++it) { + auto const_type = it->first; + auto& addr_info = it->second; + + if (addr_info.code_offset == 0) { + // constant is not defined in this map + continue; + } + + const_ofs = code_offset + addr_info.code_offset; + + bool extract_constant = false; + /*if (addr_info.pre_data == ExeConstants::magic_prev && extract_success) { + extract_constant = true; + } else*/ + if (match_surrounding_data(addr_info, const_ofs)) { + extract_constant = true; + } + + if (extract_constant) { + int32_t value; + switch (addr_info.size_val) { + case 4: + value = GetU32(const_ofs); + break; + case 2: + value = GetU16(const_ofs); + break; + case 1: + value = GetU8(const_ofs); + break; + default: + continue; + } + + auto it = game_constants.find(const_type); + if (it != game_constants.end() && it->second == value) { + // Constant override has already been applied through some other means + continue; + } + + if (value != addr_info.default_val || it != game_constants.end()) { + game_constants[const_type] = value; + Output::Debug("Read constant '{}': {} (default: {})", EXE::Shared::kGameConstantType.tag(const_type), value, addr_info.default_val); + } else if (debug_const_extraction) { + Output::Debug("Constant '{}' unchanged: {}", EXE::Shared::kGameConstantType.tag(const_type), value); + } + extract_success = true; + } else { + Output::Debug("Could not read constant '{}'", EXE::Shared::kGameConstantType.tag(const_type)); + extract_success = false; + } + } + } else { + Output::Debug("Unknown build"); + } + return game_constants; +} + +std::map EXEReader::GetEmbeddedStrings(std::string encoding) { + constexpr int max_string_size = 32; + constexpr bool debug_string_extraction = true; + + std::map embedded_strings; + int code_offset = file_info.code_ofs - 0x400; + std::array str_data; + + auto match_surrounding_data = [&](const EXE::BuildInfo::CodeAddressStringInfo& info, const uint32_t const_ofs) { + for (int i = 0; i < info.pre_data.size(); i++) { + if (info.pre_data[i] != GetU8(const_ofs - info.pre_data.size() + i)) + return false; + } + return true; + }; + + auto check_string_address_map = [&](const EXE::Strings::string_address_map& map) { + uint32_t const_ofs; + bool extract_success = false; + + for (auto it = map.begin(); it != map.end(); ++it) { + auto const_type = it->first; + auto& addr_info = it->second; + + if (addr_info.code_offset == 0) { + // string is not defined in this map + continue; + } + + const_ofs = code_offset + addr_info.code_offset; + + bool extract_string = false; + if (match_surrounding_data(addr_info, const_ofs)) { + extract_string = true; + } + + if (extract_string) { + int32_t size_str = GetU32(const_ofs); + if (size_str > max_string_size) { + Output::Debug("Unexpected length for embedded string: {} ({})", EXE::Shared::kEmbeddedStringTypes.tag(const_type), size_str); + continue; + } + const_ofs += 4; + for (int i = 0; i < size_str; ++i) { + str_data[i] = GetU8(const_ofs + i); + } + auto crc = static_cast(crc32(0, reinterpret_cast(str_data.data()), size_str)); + + if ((crc != addr_info.crc_jp && crc != addr_info.crc_en) || debug_string_extraction) { + auto extracted_string = lcf::ReaderUtil::Recode(ToString(lcf::DBString(str_data.data(), static_cast(size_str))), encoding); + + if (debug_string_extraction && crc == addr_info.crc_jp) { + Output::Debug("Embedded string for '{}' matches JP -> '{}'", EXE::Shared::kEmbeddedStringTypes.tag(const_type), extracted_string); + } else if (debug_string_extraction && crc == addr_info.crc_en) { + Output::Debug("Embedded string for '{}' matches EN -> '{}'", EXE::Shared::kEmbeddedStringTypes.tag(const_type), extracted_string); + } else { + Output::Debug("Read embedded string '{}' -> '{}'", EXE::Shared::kEmbeddedStringTypes.tag(const_type), extracted_string); + + //TODO: add to map + } + } + + extract_success = true; + } else { + Output::Debug("Could not read embedded string '{}'", EXE::Shared::kEmbeddedStringTypes.tag(const_type)); + extract_success = false; + } + } + }; + + if (build_version == EXE::BuildInfo::KnownEngineBuildVersions::UnknownBuild) { + GetEngineBuildVersion(); + } + + switch (build_version) { + case EXE::BuildInfo::RM2K_20030625: + check_string_address_map(EXE::Strings::string_addresses_rm2k_151); + break; + default: + break; + } + + return embedded_strings; +} + +std::vector EXEReader::CheckForPatches() { + std::vector patches; + + int code_offset = file_info.code_ofs - 0x400; + + auto check_for_patch_segment = [&](const EXE::BuildInfo::PatchDetectionInfo& patch_info) { + for (int i = 0; i < patch_info.chk_segment_data.size(); i++) { + if (patch_info.chk_segment_data[i] != GetU8(code_offset + patch_info.chk_segment_offset + i)) + return false; + } + }; + + if (build_version == EXE::BuildInfo::KnownEngineBuildVersions::UnknownBuild) { + GetEngineBuildVersion(); + } + + auto& patch_detection_map = EXE::Patches::GetPatchesForBuildVersion(build_version); + + for (auto it = patch_detection_map.begin(); it < patch_detection_map.end(); ++it) { + auto patch_type = it->first; + auto& patch_info = it->second; + + if (!check_for_patch_segment(patch_info)) { + continue; + } + + if (patch_info.patch_args_size == 0) { + Output::Debug("Detected Patch: '{}'", EXE::Shared::kKnownPatches.tag(static_cast(patch_type))); + patches.emplace_back(EXE::Shared::PatchSetupInfo(patch_type)); + } else { + std::vector patch_vars; + patch_vars.resize(patch_info.patch_args_size); + + for (int i = 0; i < patch_info.patch_args_size; ++i) { + patch_vars[i] = GetU32(code_offset + patch_info.patch_args[i].offset); + } + if (patch_info.patch_args_size == 1) { + Output::Debug("Detected Patch: '{}' (VarId: {})", EXE::Shared::kKnownPatches.tag(static_cast(patch_type)), patch_vars[0]); + } else { + std::string out = "("; + for (int i = 0; i < patch_vars.size(); ++i) { + if (i > 0) { + out += ", "; + } + out += fmt::format("{}", patch_vars[i]); + } + out += ")"; + Output::Debug("Detected Patch: '{}' (Config: {})", EXE::Shared::kKnownPatches.tag(static_cast(patch_type)), out); + } + patches.emplace_back(EXE::Shared::PatchSetupInfo(patch_type, patch_vars)); + } + } + + return patches; +} + +bool EXEReader::CheckForString(uint32_t offset, const char* p) { + while (*p) { + if (GetU8(file_info.code_ofs - 0x400 + offset++) != *p++) + return false; + } + return true; +} #endif diff --git a/src/exe_reader.h b/src/exe_reader.h index 15abb5ded8..ba5c630e7a 100644 --- a/src/exe_reader.h +++ b/src/exe_reader.h @@ -23,6 +23,10 @@ #include #include #include "bitmap.h" +#include "exe_buildinfo.h" +#include "exe_constants.h" +#include "exe_patches.h" +#include "exe_strings.h" #include "player.h" /** @@ -64,6 +68,8 @@ class EXEReader { uint32_t geep_size = 0; MachineType machine_type = MachineType::Unknown; bool is_easyrpg_player = false; + uint32_t code_ofs = 0; + uint32_t entrypoint = 0; int GetEngineType(bool& is_maniac_patch) const; void Print() const; @@ -71,6 +77,14 @@ class EXEReader { const FileInfo& GetFileInfo(); + EXE::BuildInfo::KnownEngineBuildVersions GetEngineBuildVersion(); + + std::map GetOverriddenGameConstants(); + + std::map GetEmbeddedStrings(std::string encoding); + + std::vector CheckForPatches(); + private: // Bounds-checked unaligned reader primitives. // In case of out-of-bounds, returns 0 - this will usually result in a harmless error at some other level, @@ -83,12 +97,18 @@ class EXEReader { uint32_t GetLogoCount(); bool ResNameCheck(uint32_t namepoint, const char* name); + bool CheckForString(uint32_t offset, const char* p); + // 0 if resource section was unfindable. uint32_t resource_ofs = 0; uint32_t resource_rva = 0; FileInfo file_info; Filesystem_Stream::InputStream corefile; + + EXE::BuildInfo::KnownEngineBuildVersions build_version = EXE::BuildInfo::KnownEngineBuildVersions::UnknownBuild; + EXE::BuildInfo::EngineBuildInfo build_info; + }; #endif diff --git a/src/exe_shared.h b/src/exe_shared.h new file mode 100644 index 0000000000..18abb07488 --- /dev/null +++ b/src/exe_shared.h @@ -0,0 +1,186 @@ +/* + * 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_EXE_SHARED_H +#define EP_EXE_SHARED_H + +#include +#include +#include +#include + +namespace EXE::Shared { + + enum class GameConstantType { + MinVarLimit, + MaxVarLimit, + MaxActorHP, + MaxActorSP, + MaxEnemyHP, + MaxEnemySP, + + MaxAtkBaseValue, + MaxDefBaseValue, + MaxSpiBaseValue, + MaxAgiBaseValue, + + MaxAtkBattleValue, + MaxDefBattleValue, + MaxSpiBattleValue, + MaxAgiBattleValue, + + MaxDamageValue, + MaxExpValue, + MaxLevel, + MaxGoldValue, + MaxItemCount, + MaxSaveFiles, + + /** X-coordinate of the title scene command window (HAlign: Center) */ + TitleX, + /** Y-coordinate of the title scene command window (VAlign: Top) */ + TitleY, + /** X-coordinate of the title scene command window when the title graphic is hidden (HAlign: Center) */ + TitleHiddenX, + /** Y-coordinate of the title scene command window when the title graphic is hidden (VAlign: Top) */ + TitleHiddenY, + + LAST + }; + + static constexpr auto kGameConstantType = lcf::makeEnumTags( + "MinVarLimit", + "MaxVarLimit", + "MaxActorHP", + "MaxActorSP", + "MaxEnemyHP", + "MaxEnemySP", + + "MaxAtkBaseValue", + "MaxDefBaseValue", + "MaxSpiBaseValue", + "MaxAgiBaseValue", + + "MaxAtkBattleValue", + "MaxDefBattleValue", + "MaxSpiBattleValue", + "MaxAgiBattleValue", + + "MaxDamageValue", + "MaxExpValue", + "MaxLevel", + "MaxGoldValue", + "MaxItemCount", + "MaxSaveFiles", + "TitleCmdWnd_X", + "TitleCmdWnd_Y", + "TitleHiddenCmdWnd_X", + "TitleHiddenCmdWnd_Y" + ); + + static_assert(kGameConstantType.size() == static_cast(GameConstantType::LAST)); + + enum class EmbeddedStringTypes { + Battle_DamageToEnemy, + Battle_DamageToAlly, + Battle_HpSpRecovery, + Battle_StatDecrease, + Battle_StatIncrease, + Battle_AbsorbEnemy, + Battle_AbsorbAlly, + Battle_UseItem, + Msg_LevelUp, + Menu_ExpMaxedOut, + Menu_2k3ActorStatus_ExpMaxedOut, + LAST + }; + + static constexpr auto kEmbeddedStringTypes = lcf::makeEnumTags( + "Battle_DamageToEnemy", + "Battle_DamageToAlly", + "Battle_HpSpRecovery", + "Battle_StatDecrease", + "Battle_StatIncrease", + "Battle_AbsorbEnemy", + "Battle_AbsorbAlly", + "Battle_UseItem", + "Msg_LevelUp", + "Menu_ExpMaxedOut", + "Menu_2k3ActorStatus_ExpMaxedOut" + ); + + static_assert(kEmbeddedStringTypes.size() == static_cast(EmbeddedStringTypes::LAST)); + + enum class KnownPatches { + UnlockPics, + CommonThisEvent, + BreakLoopFix, + AutoEnterPatch, + BetterAEP, + PicPointer, + PicPointer_R, + DirectMenu, + MonSca, + MonScaPlus, + EXPlus, + EXPlusPlus, + + LAST + }; + + static constexpr auto kKnownPatches = lcf::makeEnumTags( + "UnlockPics", + "CommonThisEvent", + "BreakLoopFix", + "AutoEnterPatch", + "BetterAEP", + "PicPointer 2.5b", + "PicPointer Restruct", + "DirectMenu", + "MonSca", + "MonScaPlus", + "EXPlus", + "EXPlusPlus" + ); + + static_assert(kKnownPatches.size() == static_cast(KnownPatches::LAST)); + + struct PatchSetupInfo { + KnownPatches patch_type; + std::vector customizations; + + PatchSetupInfo() + : patch_type(static_cast(-1)), customizations({}) { + } + + PatchSetupInfo(KnownPatches patch_type) + : patch_type(patch_type), customizations({}) { + } + + PatchSetupInfo(KnownPatches patch_type, std::vector customizations) + : patch_type(patch_type), customizations(customizations) { + } + }; + + struct EngineCustomization { + std::map constant_overrides; + std::map strings; + std::map runtime_patches; + }; +} + +#endif diff --git a/src/exe_strings.h b/src/exe_strings.h new file mode 100644 index 0000000000..96398acf1d --- /dev/null +++ b/src/exe_strings.h @@ -0,0 +1,61 @@ +/* + * 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_EXE_STRINGS_H +#define EP_EXE_STRINGS_H + +#include "exe_buildinfo.h" +#include "exe_shared.h" + +namespace EXE::Strings { + using namespace BuildInfo; + using EmbeddedStringTypes = EXE::Shared::EmbeddedStringTypes; + + using string_address = std::pair; + using string_address_map = std::array(EmbeddedStringTypes::LAST)>; + + template + constexpr string_address map(size_t code_offset, uint32_t crc_jp, uint32_t crc_en, Args&&... args) { + return { C, CodeAddressStringInfo(code_offset, crc_jp, crc_en, std::forward(args)...) }; + } + + template + constexpr string_address not_def() { + return { C, CodeAddressStringInfo(0, 0, 0, 0) }; + } + + using Str = EmbeddedStringTypes; + +#define FFFFFFFF 0xFF, 0xFF, 0xFF, 0xFF + + //TODO: offsets: 0x07B848, 0x07B860 (are these strings used anywhere?) + constexpr string_address_map string_addresses_rm2k_151 = {{ + map(0x07AEA4, 0x9D4F8EDE, 0xB55355BF, FFFFFFFF), + map(0x07AEB8, 0x6900AACD, 0xB55355BF, FFFFFFFF), + map(0x07AFF0, 0x7C968020, 0x5299B765, FFFFFFFF), + map(0x07B1E0, 0x7C968020, 0x5299B765, FFFFFFFF), + map(0x07B43C, 0x7C968020, 0x5299B765, FFFFFFFF), + map(0x07B65C, 0xFB3336D9, 0x5299B765, FFFFFFFF), + map(0x07B674, 0x3ABDE919, 0x5299B765, FFFFFFFF), + not_def(), + map(0x0882CC, 0x0DED4C01, 0x5299B765, FFFFFFFF), + map(0x07C678, 0x2770FF3E, 0x2770FF3E, FFFFFFFF), //TODO + not_def() + }}; +} + +#endif diff --git a/src/filefinder.cpp b/src/filefinder.cpp index ccc2b841e5..971fe763b5 100644 --- a/src/filefinder.cpp +++ b/src/filefinder.cpp @@ -399,7 +399,7 @@ bool FileFinder::HasSavegame() { int FileFinder::GetSavegames() { auto fs = Save(); - for (int i = 1; i <= 15; i++) { + for (int i = 1; i <= Player::Constants::MaxSaveFiles(); i++) { std::stringstream ss; ss << "Save" << (i <= 9 ? "0" : "") << i << ".lsd"; std::string filename = fs.FindFile(ss.str()); diff --git a/src/game_actor.cpp b/src/game_actor.cpp index 4543a181c9..e46bddff60 100644 --- a/src/game_actor.cpp +++ b/src/game_actor.cpp @@ -35,47 +35,16 @@ #include "rand.h" #include "algo.h" -constexpr int max_level_2k = 50; -constexpr int max_level_2k3 = 99; - int Game_Actor::MaxHpValue() const { - auto& val = lcf::Data::system.easyrpg_max_actor_hp; - if (val == -1) { - return Player::IsRPG2k() ? 999 : 9999; - } - return val; + return Player::Constants::MaxActorHpValue(); } int Game_Actor::MaxSpValue() const { - auto& val = lcf::Data::system.easyrpg_max_actor_sp; - if (val == -1) { - return 999; - } - return val; -} - -int Game_Actor::MaxStatBattleValue() const { - auto& val = lcf::Data::system.easyrpg_max_stat_battle_value; - if (val == -1) { - return 9999; - } - return val; -} - -int Game_Actor::MaxStatBaseValue() const { - auto& val = lcf::Data::system.easyrpg_max_stat_base_value; - if (val == -1) { - return 999; - } - return val; + return Player::Constants::MaxActorSpValue(); } int Game_Actor::MaxExpValue() const { - auto& val = lcf::Data::system.easyrpg_max_exp; - if (val == -1) { - return Player::IsRPG2k() ? 999999 : 9999999; - } - return val; + return Player::Constants::MaxExpValue(); } Game_Actor::Game_Actor(int actor_id) { @@ -511,7 +480,7 @@ int Game_Actor::GetBaseAtk(Weapon weapon, bool mod, bool equip) const { ForEachEquipment(GetWholeEquipment(), [&](auto& item) { n += item.atk_points1; }, weapon); } - return Utils::Clamp(n, 1, MaxStatBaseValue()); + return Utils::Clamp(n, 1, Player::Constants::MaxAtkBaseValue()); } int Game_Actor::GetBaseAtk(Weapon weapon) const { @@ -534,7 +503,7 @@ int Game_Actor::GetBaseDef(Weapon weapon, bool mod, bool equip) const { ForEachEquipment(GetWholeEquipment(), [&](auto& item) { n += item.def_points1; }, weapon); } - return Utils::Clamp(n, 1, MaxStatBaseValue()); + return Utils::Clamp(n, 1, Player::Constants::MaxDefBaseValue()); } int Game_Actor::GetBaseDef(Weapon weapon) const { @@ -557,7 +526,7 @@ int Game_Actor::GetBaseSpi(Weapon weapon, bool mod, bool equip) const { ForEachEquipment(GetWholeEquipment(), [&](auto& item) { n += item.spi_points1; }, weapon); } - return Utils::Clamp(n, 1, MaxStatBaseValue()); + return Utils::Clamp(n, 1, Player::Constants::MaxSpiBaseValue()); } int Game_Actor::GetBaseSpi(Weapon weapon) const { @@ -580,7 +549,7 @@ int Game_Actor::GetBaseAgi(Weapon weapon, bool mod, bool equip) const { ForEachEquipment(GetWholeEquipment(), [&](auto& item) { n += item.agi_points1; }, weapon); } - return Utils::Clamp(n, 1, MaxStatBaseValue()); + return Utils::Clamp(n, 1, Player::Constants::MaxAgiBaseValue()); } int Game_Actor::GetBaseAgi(Weapon weapon) const { @@ -744,11 +713,7 @@ int Game_Actor::GetAccessoryId() const { } int Game_Actor::GetMaxLevel() const { - int max_level = Player::IsRPG2k() ? max_level_2k : max_level_2k3; - if (lcf::Data::system.easyrpg_max_level > -1) { - max_level = lcf::Data::system.easyrpg_max_level; - } - return Utils::Clamp(max_level, 1, dbActor->final_level); + return Utils::Clamp(Player::Constants::MaxLevel(), 1, dbActor->final_level); } void Game_Actor::SetExp(int _exp) { @@ -805,6 +770,15 @@ std::string Game_Actor::GetLevelUpMessage(int new_level) const { Utils::MakeSvArray(GetName(), ss.str(), lcf::Data::terms.level) ); } else { + StringView template_text; + if (Player::Constants::HasEmbeddedTemplateString(EXE::Shared::EmbeddedStringTypes::Msg_LevelUp, template_text)) { + return Utils::ReplacePlaceholders( + template_text, + Utils::MakeArray('S', 'L', 'V', 'T'), + Utils::MakeSvArray(GetName(), ToString(lcf::Data::terms.use_item), std::to_string(new_level), ToString(lcf::Data::terms.level_up)) + ); + } + std::string particle, space = ""; if (Player::IsCP932()) { particle = "は"; @@ -1161,11 +1135,6 @@ static int ClampMaxSpMod(int sp, const Game_Actor* actor) { return Utils::Clamp(sp, -limit, limit); } -static int ClampStatMod(int value, const Game_Actor* actor) { - auto limit = actor->MaxStatBaseValue(); - return Utils::Clamp(value, -limit, limit); -} - void Game_Actor::SetBaseMaxHp(int maxhp) { int new_hp_mod = data.hp_mod + (maxhp - GetBaseMaxHp()); data.hp_mod = ClampMaxHpMod(new_hp_mod, this); @@ -1192,22 +1161,22 @@ int Game_Actor::SetSp(int sp) { void Game_Actor::SetBaseAtk(int atk) { int new_attack_mod = data.attack_mod + (atk - GetBaseAtk()); - data.attack_mod = ClampStatMod(new_attack_mod, this); + data.attack_mod = Utils::Clamp(new_attack_mod, -Player::Constants::MaxAtkBaseValue(), Player::Constants::MaxAtkBaseValue()); } void Game_Actor::SetBaseDef(int def) { int new_defense_mod = data.defense_mod + (def - GetBaseDef()); - data.defense_mod = ClampStatMod(new_defense_mod, this); + data.defense_mod = Utils::Clamp(new_defense_mod, -Player::Constants::MaxDefBaseValue(), Player::Constants::MaxDefBaseValue()); } void Game_Actor::SetBaseSpi(int spi) { int new_spirit_mod = data.spirit_mod + (spi - GetBaseSpi()); - data.spirit_mod = ClampStatMod(new_spirit_mod, this); + data.spirit_mod = Utils::Clamp(new_spirit_mod, -Player::Constants::MaxSpiBaseValue(), Player::Constants::MaxSpiBaseValue()); } void Game_Actor::SetBaseAgi(int agi) { int new_agility_mod = data.agility_mod + (agi - GetBaseAgi()); - data.agility_mod = ClampStatMod(new_agility_mod, this); + data.agility_mod = Utils::Clamp(new_agility_mod, -Player::Constants::MaxAgiBaseValue(), Player::Constants::MaxAgiBaseValue()); } Game_Actor::RowType Game_Actor::GetBattleRow() const { diff --git a/src/game_actor.h b/src/game_actor.h index c3b34e63d6..479a73ff2e 100644 --- a/src/game_actor.h +++ b/src/game_actor.h @@ -55,10 +55,6 @@ class Game_Actor final : public Game_Battler { int MaxSpValue() const override; - int MaxStatBattleValue() const override; - - int MaxStatBaseValue() const override; - int MaxExpValue() const; virtual PermanentStates GetPermanentStates() const override; @@ -425,7 +421,7 @@ class Game_Actor final : public Game_Battler { /** * Sets exp of actor. - * The value is adjusted to the boundary 0 up 999999. + * The value is adjusted to the boundary 0 up to a maximum (dependent on engine type & patch). * Other actor attributes are not altered. Use ChangeExp to do a proper * experience change. * diff --git a/src/game_battlealgorithm.cpp b/src/game_battlealgorithm.cpp index 6204d32f2c..ce9cc5a22d 100644 --- a/src/game_battlealgorithm.cpp +++ b/src/game_battlealgorithm.cpp @@ -52,7 +52,7 @@ #include "feature.h" static inline int MaxDamageValue() { - return lcf::Data::system.easyrpg_max_damage == -1 ? (Player::IsRPG2k() ? 999 : 9999) : lcf::Data::system.easyrpg_max_damage; + return Player::Constants::MaxDamageValue(); } Game_BattleAlgorithm::AlgorithmBase::AlgorithmBase(Type ty, Game_Battler* source, Game_Battler* target) : diff --git a/src/game_battler.cpp b/src/game_battler.cpp index d6dee13a2e..52d43d7192 100644 --- a/src/game_battler.cpp +++ b/src/game_battler.cpp @@ -544,35 +544,35 @@ static int AdjustParam(int base, int mod, int maxval, Span states } int Game_Battler::CalcValueAfterAtkStates(int value) const { - return AdjustParam(value, 0, MaxStatBattleValue(), GetInflictedStates(), &lcf::rpg::State::affect_attack); + return AdjustParam(value, 0, Player::Constants::MaxAtkBattleValue(), GetInflictedStates(), &lcf::rpg::State::affect_attack); } int Game_Battler::CalcValueAfterDefStates(int value) const { - return AdjustParam(value, 0, MaxStatBattleValue(), GetInflictedStates(), &lcf::rpg::State::affect_defense); + return AdjustParam(value, 0, Player::Constants::MaxDefBattleValue(), GetInflictedStates(), &lcf::rpg::State::affect_defense); } int Game_Battler::CalcValueAfterSpiStates(int value) const { - return AdjustParam(value, 0, MaxStatBattleValue(), GetInflictedStates(), &lcf::rpg::State::affect_spirit); + return AdjustParam(value, 0, Player::Constants::MaxSpiBattleValue(), GetInflictedStates(), &lcf::rpg::State::affect_spirit); } int Game_Battler::CalcValueAfterAgiStates(int value) const { - return AdjustParam(value, 0, MaxStatBattleValue(), GetInflictedStates(), &lcf::rpg::State::affect_agility); + return AdjustParam(value, 0, Player::Constants::MaxAgiBattleValue(), GetInflictedStates(), &lcf::rpg::State::affect_agility); } int Game_Battler::GetAtk(Weapon weapon) const { - return AdjustParam(GetBaseAtk(weapon), atk_modifier, MaxStatBattleValue(), GetInflictedStates(), &lcf::rpg::State::affect_attack); + return AdjustParam(GetBaseAtk(weapon), atk_modifier, Player::Constants::MaxAtkBattleValue(), GetInflictedStates(), &lcf::rpg::State::affect_attack); } int Game_Battler::GetDef(Weapon weapon) const { - return AdjustParam(GetBaseDef(weapon), def_modifier, MaxStatBattleValue(), GetInflictedStates(), &lcf::rpg::State::affect_defense); + return AdjustParam(GetBaseDef(weapon), def_modifier, Player::Constants::MaxDefBattleValue(), GetInflictedStates(), &lcf::rpg::State::affect_defense); } int Game_Battler::GetSpi(Weapon weapon) const { - return AdjustParam(GetBaseSpi(weapon), spi_modifier, MaxStatBattleValue(), GetInflictedStates(), &lcf::rpg::State::affect_spirit); + return AdjustParam(GetBaseSpi(weapon), spi_modifier, Player::Constants::MaxSpiBattleValue(), GetInflictedStates(), &lcf::rpg::State::affect_spirit); } int Game_Battler::GetAgi(Weapon weapon) const { - return AdjustParam(GetBaseAgi(weapon), agi_modifier, MaxStatBattleValue(), GetInflictedStates(), &lcf::rpg::State::affect_agility); + return AdjustParam(GetBaseAgi(weapon), agi_modifier, Player::Constants::MaxAgiBattleValue(), GetInflictedStates(), &lcf::rpg::State::affect_agility); } int Game_Battler::GetDisplayX() const { diff --git a/src/game_battler.h b/src/game_battler.h index 79b4acad7c..35526ef5c3 100644 --- a/src/game_battler.h +++ b/src/game_battler.h @@ -77,10 +77,6 @@ class Game_Battler { virtual int MaxSpValue() const = 0; - virtual int MaxStatBattleValue() const = 0; - - virtual int MaxStatBaseValue() const = 0; - /** * Gets if battler has a state. * diff --git a/src/game_config_game.cpp b/src/game_config_game.cpp index 4a719b5556..519782bf08 100644 --- a/src/game_config_game.cpp +++ b/src/game_config_game.cpp @@ -78,6 +78,16 @@ void Game_ConfigGame::LoadFromArgs(CmdlineParser& cp) { } continue; } + if (cp.ParseNext(arg, 1, "--engine-path")) { + if (arg.NumValues() > 0) { + std::string path = arg.Value(0); + path = FileFinder::MakeCanonical(path, 0); + if (!path.empty()) { + engine_path.Set(path); + } + } + continue; + } if (cp.ParseNext(arg, 0, "--no-patch")) { patch_support.Set(false); patch_dynrpg.Lock(false); @@ -192,6 +202,7 @@ void Game_ConfigGame::LoadFromStream(Filesystem_Stream::InputStream& is) { new_game.FromIni(ini); engine_str.FromIni(ini); + engine_path.FromIni(ini); fake_resolution.FromIni(ini); if (patch_easyrpg.FromIni(ini)) { diff --git a/src/game_config_game.h b/src/game_config_game.h index fe9e3ec0c0..dff3ac33d2 100644 --- a/src/game_config_game.h +++ b/src/game_config_game.h @@ -37,6 +37,7 @@ struct Game_ConfigGame { BoolConfigParam new_game{ "Start new game", "Skips the title screen and starts a new game directly", "Game", "NewGame", false }; StringConfigParam engine_str{ "Engine", "", "Game", "Engine", std::string() }; + StringConfigParam engine_path{ "Engine Path", "Sets the executable to be used by the engine auto-detection", "Game", "EnginePath", std::string() }; BoolConfigParam fake_resolution{ "Fake Metrics", "Makes games run on higher resolutions (with some success)", "Game", "FakeResolution", false }; BoolConfigParam patch_easyrpg{ "EasyRPG", "EasyRPG Engine Extensions", "Patch", "EasyRPG", false }; BoolConfigParam patch_destiny{ "Destiny Patch", "", "Patch", "Destiny", false }; diff --git a/src/game_dynrpg_loader.cpp b/src/game_dynrpg_loader.cpp new file mode 100644 index 0000000000..7204fe386e --- /dev/null +++ b/src/game_dynrpg_loader.cpp @@ -0,0 +1,439 @@ +/* + * 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 + +#include + +namespace { + std::map ini_quickpatches; + std::map known_patches; + + int ini_handler_qp(void* user, const char* section, const char* name, const char* value) { + if (std::strcmp(section, DYNRPG_INI_SECTION_QUICKPATCHES) == 0) { + ini_quickpatches[name] = value; + } + return 1; + } + + 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, bool& is_rle) { + is_rle = false; + + 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(); + + if (out_size == 0) { + // RLE encoded + is.seekg(pos++, std::ios_base::beg); + out_size = is.get() << 8; + is.seekg(pos++, std::ios_base::beg); + out_size |= is.get(); + is_rle = true; + } + return true; + } + + int 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(EXE::BuildInfo::KnownEngineBuildVersions build_version) { + auto dir_contents = FileFinder::Game().ListDirectory(DYNRPG_FOLDER_PATCHES); + + if (!dir_contents) { + return {}; + } + + if (build_version == EXE::BuildInfo::UnknownBuild) { + // If not otherwise specified, DynRPG should always be Rm2k3 v1.08 + build_version = EXE::BuildInfo::RM2K3_1080_1080; + } + + known_patches.clear(); + for (auto& p : EXE::Patches::GetPatchesForBuildVersion(build_version)) { + known_patches[p.first] = p.second; + } + + std::vector result; + + for (auto item : *dir_contents) { + auto filename_cp = item.first; + std::transform(filename_cp.begin(), filename_cp.end(), filename_cp.begin(), ::tolower); + if (filename_cp.find("antilag") != std::string::npos) { + // Skip all variants of the "AntiLag" patch + continue; + } + if (filename_cp.find("statdelimiter") != std::string::npos) { + // StatDelimiter's changes should already be handled by the constant extraction mechanism + continue; + } + + 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; + bool is_rle = false; + if (!read_ips_offset(is, i, address, size, is_rle)) { + 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; + + int ofs = patch_info->chk_segment_offset - address; + + for (int j = 0; j < patch_info->chk_segment_data.size(); ++j) { + if (!is_rle) { + is.seekg(i + j + ofs, std::ios_base::beg); + } + if (is.get() != patch_info->chk_segment_data[j]) { + patch_info = nullptr; + break; + } + } + if (patch_info != nullptr) { + break; + } + } + 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->patch_args_size == 0) { + Output::Debug("DynRPG Loader: Applying patch '{}'", EXE::Shared::kKnownPatches.tag(patch_type)); + return EXE::Shared::PatchSetupInfo(patch_type); + } + + i = magic_ips.size(); + std::vector var_ids; + var_ids.resize(patch_info->patch_args_size); + do { + uint32_t address = 0, size = 0; + bool is_rle = false; + if (!read_ips_offset(is, i, address, size, is_rle)) { + break; + } + + for (int j = 0; j < patch_info->patch_args_size; ++j) { + auto extract_var_offset = patch_info->patch_args[j].offset; + if (extract_var_offset >= address && extract_var_offset <= (address + size)) { + int section_ofs = extract_var_offset - address; + var_ids[j] = read_int32(is, i + section_ofs); + } + } + + 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_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; + }*/ + + if (patch_info->patch_args_size == 1) { + Output::Debug("DynRPG Loader: Applying patch '{}' with var value '{}'", EXE::Shared::kKnownPatches.tag(patch_type), var_ids[0]); + } else { + std::string out = "("; + for (int i = 0; i < var_ids.size(); ++i) { + if (i > 0) { + out += ", "; + } + out += fmt::format("{}", var_ids[i]); + } + out += ")"; + Output::Debug("DynRPG Loader: Applying patch '{}' (Config: {})", EXE::Shared::kKnownPatches.tag(patch_type), out); + } + + return EXE::Shared::PatchSetupInfo(patch_type, var_ids); +} + +void DynRpg_Loader::ApplyQuickPatches(EXE::Shared::EngineCustomization& engine_customization, EXE::BuildInfo::KnownEngineBuildVersions build_version) { + ini_quickpatches.clear(); + + int error = ini_parse(DYNRPG_INI_NAME, ini_handler_qp, nullptr); + + if (error == -1) { + return; + } + + if (ini_quickpatches.size() > 0) { + Output::Debug("Found section for QuickPatches inside DynRPG.ini."); + } + + if (build_version == EXE::BuildInfo::UnknownBuild) { + // If not otherwise specified, DynRPG should always be Rm2k3 v1.08 + 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 : ini_quickpatches) { + 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; + } + + auto found_matching_patch = EXE::Shared::KnownPatches::LAST; + std::vector quickpatched_ips_no; + std::vector quickpatched_ips_vars; + + for (auto section : qp) { + for (auto& p : loaded_ips_patches) { + if (found_matching_patch != EXE::Shared::KnownPatches::LAST && p.first != found_matching_patch) { + // Assuming that multiple addresses in a QuickPatch line target only a single IPS patch.. + break; + } + + auto& loaded_patch = p.second; + for (int i = 0; i < loaded_patch.patch_args_size; ++i) { + auto& ofs = loaded_patch.patch_args[i].offset; + if (ofs == section.address) { + found_matching_patch = p.first; + quickpatched_ips_no.push_back(i); + quickpatched_ips_vars.push_back(section.patch_val); + } + } + } + } + + if (found_matching_patch != EXE::Shared::KnownPatches::LAST) { + Output::Debug("DynRPG Loader: Found valid QuickPatch '{}' targeting known patch constants for '{}'", key_name, EXE::Shared::kKnownPatches.tag(found_matching_patch)); + if (quickpatched_ips_vars.size() == 1) { + //TODO: detailed debug output + } else { + //TODO: debug output + } + for (auto& p : engine_customization.runtime_patches) { + auto& patch = p.second; + if (patch.patch_type != found_matching_patch) { + continue; + } + for (int i = 0; i < quickpatched_ips_no.size(); ++i) { + patch.customizations[quickpatched_ips_no[i]] = quickpatched_ips_vars[i]; + } + } + 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..89d89d0b00 --- /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 "exe_buildinfo.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" + const auto invalid_patch = EXE::Shared::PatchSetupInfo(static_cast(-1)); + + std::vector DetectRuntimePatches(EXE::BuildInfo::KnownEngineBuildVersions build_version = EXE::BuildInfo::KnownEngineBuildVersions::RM2K3_1080_1080); + + EXE::Shared::PatchSetupInfo ReadIPS(std::string const& item_name, Filesystem_Stream::InputStream& is); + + void ApplyQuickPatches(EXE::Shared::EngineCustomization& engine_customization, EXE::BuildInfo::KnownEngineBuildVersions build_version = EXE::BuildInfo::KnownEngineBuildVersions::RM2K3_1080_1080); + + 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/game_enemy.cpp b/src/game_enemy.cpp index 3da6b4a7fd..d73066a631 100644 --- a/src/game_enemy.cpp +++ b/src/game_enemy.cpp @@ -48,35 +48,11 @@ Game_Enemy::Game_Enemy(const lcf::rpg::TroopMember* member) } int Game_Enemy::MaxHpValue() const { - auto& val = lcf::Data::system.easyrpg_max_enemy_hp; - if (val == -1) { - return Player::IsRPG2k() ? 9999 : 99999; - } - return val; + return Player::Constants::MaxEnemyHpValue(); } int Game_Enemy::MaxSpValue() const { - auto& val = lcf::Data::system.easyrpg_max_enemy_sp; - if (val == -1) { - return 9999; - } - return val; -} - -int Game_Enemy::MaxStatBattleValue() const { - auto& val = lcf::Data::system.easyrpg_max_stat_battle_value; - if (val == -1) { - return 9999; - } - return val; -} - -int Game_Enemy::MaxStatBaseValue() const { - auto& val = lcf::Data::system.easyrpg_max_stat_base_value; - if (val == -1) { - return 999; - } - return val; + return Player::Constants::MaxEnemySpValue(); } int Game_Enemy::GetStateProbability(int state_id) const { diff --git a/src/game_enemy.h b/src/game_enemy.h index 0821c2fed9..f243c2fa8e 100644 --- a/src/game_enemy.h +++ b/src/game_enemy.h @@ -41,10 +41,6 @@ class Game_Enemy final : public Game_Battler int MaxSpValue() const override; - int MaxStatBattleValue() const override; - - int MaxStatBaseValue() const override; - Point GetOriginalPosition() const override; void ResetBattle() override; diff --git a/src/game_interpreter.cpp b/src/game_interpreter.cpp index 5a5b6dbed7..80e7f35e86 100644 --- a/src/game_interpreter.cpp +++ b/src/game_interpreter.cpp @@ -3761,7 +3761,7 @@ bool Game_Interpreter::CommandBreakLoop(lcf::rpg::EventCommand const& /* com */) // BreakLoop will jump to the end of the event if there is no loop. - bool has_bug = !Player::IsPatchManiac(); + bool has_bug = !Player::HasBreakLoopFix(); if (!has_bug) { SkipToNextConditional({ Cmd::EndLoop }, list[index].indent - 1); ++index; diff --git a/src/game_party.cpp b/src/game_party.cpp index bfd8e9dd8e..dcfcd2ccce 100644 --- a/src/game_party.cpp +++ b/src/game_party.cpp @@ -161,7 +161,7 @@ int Game_Party::GetItemTotalCount(int item_id) const { int Game_Party::GetMaxItemCount(int item_id) const { const lcf::rpg::Item* item = lcf::ReaderUtil::GetElement(lcf::Data::items, item_id); if (!item || item->easyrpg_max_count == -1) { - return (lcf::Data::system.easyrpg_max_item_count == -1 ? 99 : lcf::Data::system.easyrpg_max_item_count); + return Player::Constants::MaxItemCount(); } else { return item->easyrpg_max_count; } @@ -169,12 +169,12 @@ int Game_Party::GetMaxItemCount(int item_id) const { void Game_Party::GainGold(int n) { data.gold = data.gold + n; - data.gold = std::min(std::max(data.gold, 0), 999999); + data.gold = std::min(std::max(data.gold, 0), Player::Constants::MaxGoldValue()); } void Game_Party::LoseGold(int n) { data.gold = data.gold - n; - data.gold = std::min(std::max(data.gold, 0), 999999); + data.gold = std::min(std::max(data.gold, 0), Player::Constants::MaxGoldValue()); } void Game_Party::AddItem(int item_id, int amount) { diff --git a/src/game_variables.h b/src/game_variables.h index 0d6e9382e2..15ac1fa81d 100644 --- a/src/game_variables.h +++ b/src/game_variables.h @@ -34,10 +34,10 @@ class Game_Variables { using Variables_t = std::vector; static constexpr int max_warnings = 10; - static constexpr Var_t min_2k = -999999; - static constexpr Var_t max_2k = 999999; - static constexpr Var_t min_2k3 = -9999999; - static constexpr Var_t max_2k3 = 9999999; + static constexpr Var_t min_2k = -999'999; + static constexpr Var_t max_2k = 999'999; + static constexpr Var_t min_2k3 = -9'999'999; + static constexpr Var_t max_2k3 = 9'999'999; Game_Variables(Var_t minval, Var_t maxval); diff --git a/src/meta.cpp b/src/meta.cpp index bc51999476..3987fa9c46 100644 --- a/src/meta.cpp +++ b/src/meta.cpp @@ -173,7 +173,7 @@ std::vector Meta::BuildImportCandidateList(const FilesystemView& if (is_match) { // Scan over every possible save file and see if any match. - for (int saveId = 0; saveId < 15; saveId++) { + for (int saveId = 0; saveId < Player::Constants::MaxSaveFiles(); saveId++) { std::stringstream ss; ss << "Save" << (saveId <= 8 ? "0" : "") << (saveId + 1) << ".lsd"; diff --git a/src/platform/sdl/main.cpp b/src/platform/sdl/main.cpp index aeffa5b805..97114567af 100644 --- a/src/platform/sdl/main.cpp +++ b/src/platform/sdl/main.cpp @@ -79,8 +79,15 @@ extern "C" int main(int argc, char* argv[]) { EpAndroid::env = (JNIEnv*)SDL_AndroidGetJNIEnv(); #endif + bool detect_engine = args.end() != std::find(args.begin(), args.end(), "--detect-engine"); + Player::Init(std::move(args)); - Player::Run(); + + if (detect_engine) { + Player::PrintEngineInfo(); + } else { + Player::Run(); + } // Close return Player::exit_code; diff --git a/src/player.cpp b/src/player.cpp index eedb575968..6004a12564 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" @@ -135,6 +136,7 @@ namespace Player { int rng_seed = -1; Game_ConfigPlayer player_config; Game_ConfigGame game_config; + bool break_loop_fix; #ifdef EMSCRIPTEN std::string emscripten_game_name; #endif @@ -759,14 +761,48 @@ void Player::CreateGameObjects() { if (!exfont_stream) { // Backwards compatible with older Player versions exfont_stream = FileFinder::OpenImage(".", "ExFont"); + } else { + Output::Debug("Using custom ExFont: {}", FileFinder::GetPathInsideGamePath(exfont_stream.GetName())); + 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::PrintActiveOverrides(); + + ResetGameObjects(); + + LoadFonts(); + + if (Player::IsPatchKeyPatch()) { + Main_Data::game_ineluki->ExecuteScriptList(FileFinder::Game().FindFile("autorun.script")); + } + + if (Player::IsPatchDestiny()) { + Main_Data::game_destiny->Load(); + } +} + +void Player::DetectEngine(bool ignore_patch_override) { int& engine = game_config.engine; + auto engine_build = EXE::BuildInfo::KnownEngineBuildVersions::UnknownBuild; + EXE::Shared::EngineCustomization engine_customization; + #ifndef EMSCRIPTEN // Attempt reading ExFont and version information from RPG_RT.exe (not supported on Emscripten) std::unique_ptr exe_reader; - auto exeis = FileFinder::Game().OpenFile(EXE_NAME); + const auto exe_file = game_config.engine_path.Get().empty() ? EXE_NAME : game_config.engine_path.Get(); + + if (!game_config.engine_path.Get().empty()) { + Output::Debug("Using specified .EXE '{}' for engine detection", exe_file); + } + + auto exeis = FileFinder::Game().OpenFile(exe_file); if (exeis) { exe_reader.reset(new EXEReader(std::move(exeis))); @@ -777,10 +813,24 @@ void Player::CreateGameObjects() { version_info.Print(); bool is_patch_maniac; engine = version_info.GetEngineType(is_patch_maniac); - if (!game_config.patch_override) { + if (!game_config.patch_override || ignore_patch_override) { game_config.patch_maniac.Set(is_patch_maniac); } } + engine_build = exe_reader->GetEngineBuildVersion(); + for (auto p : exe_reader->GetOverriddenGameConstants()) { + engine_customization.constant_overrides[p.first] = p.second; + } + + if (!game_config.patch_override || ignore_patch_override) { + for (auto p : exe_reader->CheckForPatches()) { + engine_customization.runtime_patches[p.patch_type] = p; + } + } + + 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"); @@ -790,11 +840,6 @@ void Player::CreateGameObjects() { } #endif - if (exfont_stream) { - Output::Debug("Using custom ExFont: {}", FileFinder::GetPathInsideGamePath(exfont_stream.GetName())); - Cache::exfont_custom = Utils::ReadStream(exfont_stream); - } - if (engine == EngineNone) { if (lcf::Data::system.ldb_id == 2003) { engine = EngineRpg2k3; @@ -816,9 +861,7 @@ void Player::CreateGameObjects() { Output::Debug("Engine configured as: 2k={} 2k3={} MajorUpdated={} Eng={}", Player::IsRPG2k(), Player::IsRPG2k3(), Player::IsMajorUpdatedVersion(), Player::IsEnglish()); - Main_Data::filefinder_rtp = std::make_unique(no_rtp_flag, no_rtp_warning_flag, rtp_path); - - if (!game_config.patch_override) { + if (!game_config.patch_override || ignore_patch_override) { if (!FileFinder::Game().FindFile("harmony.dll").empty()) { game_config.patch_key_patch.Set(true); } @@ -835,23 +878,84 @@ void Player::CreateGameObjects() { if (!FileFinder::Game().FindFile(DESTINY_DLL).empty()) { game_config.patch_destiny.Set(true); } - } - game_config.PrintActivePatches(); +#ifndef EMSCRIPTEN + if (game_config.patch_dynrpg.Get()) { + for (auto p : DynRpg_Loader::DetectRuntimePatches(engine_build)) { + engine_customization.runtime_patches[p.patch_type] = p; + } + DynRpg_Loader::ApplyQuickPatches(engine_customization, engine_build); + } +#endif - ResetGameObjects(); + using Patch = EXE::Patches::KnownPatches; - LoadFonts(); + 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; - if (Player::IsPatchKeyPatch()) { - Main_Data::game_ineluki->ExecuteScriptList(FileFinder::Game().FindFile("autorun.script")); + switch (static_cast(patch.patch_type)) { + case Patch::UnlockPics: + if (!game_config.patch_unlock_pics.Get()) { + game_config.patch_unlock_pics.Set(true); + } + break; + case Patch::CommonThisEvent: + if (!game_config.patch_common_this_event.Get()) { + game_config.patch_common_this_event.Set(true); + } + break; + case Patch::BreakLoopFix: + break_loop_fix = true; + break; + case Patch::AutoEnterPatch: + game_config.new_game.Set(true); + break; + case Patch::BetterAEP: + game_config.new_game.Set(true); + // FIXME: implement BetterAEP's extended patch functionality + // -> patch_var (default: 3350) + break; + case Patch::PicPointer: + case Patch::PicPointer_R: + //TODO + break; + case Patch::DirectMenu: + if (!game_config.patch_direct_menu.Get()) { + game_config.patch_direct_menu.Set(patch.customizations[0]); + } + break; + } + } + } } - if (Player::IsPatchDestiny()) { - Main_Data::game_destiny->Load(); + 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); + } } } +void Player::PrintEngineInfo() { + auto fs = FileFinder::Game(); + + if (!fs) { + fs = FileFinder::Root().Create(Main_Data::GetDefaultProjectPath()); + if (!fs) { + Output::Error("{} is not a valid path", Main_Data::GetDefaultProjectPath()); + } + FileFinder::SetGameFilesystem(fs); + } + // Parse game specific settings + CmdlineParser cp(arguments); + game_config = Game_ConfigGame::Create(cp); + + DetectEngine(true); + + // TODO +} + bool Player::ChangeResolution(int width, int height) { if (!DisplayUi->ChangeDisplaySurfaceResolution(width, height)) { Output::Warning("Resolution change to {}x{} failed", width, height); @@ -892,22 +996,9 @@ void Player::ResetGameObjects() { Main_Data::game_switches = std::make_unique(); Main_Data::game_switches->SetLowerLimit(lcf::Data::switches.size()); - auto min_var = lcf::Data::system.easyrpg_variable_min_value; - if (min_var == 0) { - if ((Player::game_config.patch_maniac.Get() & 1) == 1) { - min_var = std::numeric_limits::min(); - } else { - min_var = Player::IsRPG2k3() ? Game_Variables::min_2k3 : Game_Variables::min_2k; - } - } - auto max_var = lcf::Data::system.easyrpg_variable_max_value; - if (max_var == 0) { - if ((Player::game_config.patch_maniac.Get() & 1) == 1) { - max_var = std::numeric_limits::max(); - } else { - max_var = Player::IsRPG2k3() ? Game_Variables::max_2k3 : Game_Variables::max_2k; - } - } + int32_t min_var, max_var; + Player::Constants::GetVariableLimits(min_var, max_var); + Main_Data::game_variables = std::make_unique(min_var, max_var); Main_Data::game_variables->SetLowerLimit(lcf::Data::variables.size()); @@ -1416,6 +1507,8 @@ Engine options: rpg2k3 - RPG Maker 2003 (v1.00 - v1.04) rpg2k3v105 - RPG Maker 2003 (v1.05 - v1.09a) rpg2k3e - RPG Maker 2003 (English release, v1.12) + --engine-path EXE Set a custom path for the executable which is to be used + by the automatic engine detection. --font1 FILE Font to use for the first font. The system graphic of the game determines whether font 1 or 2 is used. --font1-size PX Size of font 1 in pixel. The default is 12. @@ -1597,3 +1690,298 @@ std::string Player::GetEngineVersion() { if (EngineVersion() > 0) return std::to_string(EngineVersion()); return std::string(); } + +namespace Player::Constants { + std::map constant_overrides; +} + +void Player::Constants::GetVariableLimits(Var_t& min_var, Var_t& max_var) { + + min_var = lcf::Data::system.easyrpg_variable_min_value; + TryGetOverriddenConstant(EXE::Shared::GameConstantType::MinVarLimit, min_var); + if (min_var == 0) { + if (Player::IsPatchManiac()) { + min_var = std::numeric_limits::min(); + } else { + min_var = Player::IsRPG2k3() ? Game_Variables::min_2k3 : Game_Variables::min_2k; + } + } + max_var = lcf::Data::system.easyrpg_variable_max_value; + TryGetOverriddenConstant(EXE::Shared::GameConstantType::MaxVarLimit, max_var); + if (max_var == 0) { + if (Player::IsPatchManiac()) { + max_var = std::numeric_limits::max(); + } else { + max_var = Player::IsRPG2k3() ? Game_Variables::max_2k3 : Game_Variables::max_2k; + } + } +} + +int32_t Player::Constants::MaxActorHpValue() { + auto& val = lcf::Data::system.easyrpg_max_actor_hp; + TryGetOverriddenConstant(EXE::Shared::GameConstantType::MaxActorHP, val); + if (val == -1) { + return Player::IsRPG2k() ? 999 : 9'999; + } + return val; +} + +int32_t Player::Constants::MaxActorSpValue() { + auto& val = lcf::Data::system.easyrpg_max_actor_sp; + TryGetOverriddenConstant(EXE::Shared::GameConstantType::MaxActorSP, val); + if (val == -1) { + return 999; + } + return val; +} + +int32_t Player::Constants::MaxEnemyHpValue() { + auto& val = lcf::Data::system.easyrpg_max_enemy_hp; + TryGetOverriddenConstant(EXE::Shared::GameConstantType::MaxEnemyHP, val); + if (val == -1) { + return Player::IsRPG2k() ? 9'999 : 99'999; + } + return val; +} + +int32_t Player::Constants::MaxEnemySpValue() { + auto& val = lcf::Data::system.easyrpg_max_enemy_sp; + TryGetOverriddenConstant(EXE::Shared::GameConstantType::MaxEnemySP, val); + if (val == -1) { + return 9'999; + } + return val; +} + +int32_t Player::Constants::MaxAtkBaseValue() { + auto& val = lcf::Data::system.easyrpg_max_stat_base_value; + TryGetOverriddenConstant(EXE::Shared::GameConstantType::MaxAtkBaseValue, val); + if (val == -1) { + return max_stat_base_value; + } + return val; +} + +int32_t Player::Constants::MaxDefBaseValue() { + auto& val = lcf::Data::system.easyrpg_max_stat_base_value; + TryGetOverriddenConstant(EXE::Shared::GameConstantType::MaxDefBaseValue, val); + if (val == -1) { + return max_stat_base_value; + } + return val; +} + +int32_t Player::Constants::MaxSpiBaseValue() { + auto& val = lcf::Data::system.easyrpg_max_stat_base_value; + TryGetOverriddenConstant(EXE::Shared::GameConstantType::MaxSpiBaseValue, val); + if (val == -1) { + return max_stat_base_value; + } + return val; +} + +int32_t Player::Constants::MaxAgiBaseValue() { + auto& val = lcf::Data::system.easyrpg_max_stat_base_value; + TryGetOverriddenConstant(EXE::Shared::GameConstantType::MaxAgiBaseValue, val); + if (val == -1) { + return max_stat_base_value; + } + return val; +} + +int32_t Player::Constants::MaxAtkBattleValue() { + auto& val = lcf::Data::system.easyrpg_max_stat_battle_value; + TryGetOverriddenConstant(EXE::Shared::GameConstantType::MaxAtkBattleValue, val); + if (val == -1) { + return max_stat_battle_value; + } + return val; +} + +int32_t Player::Constants::MaxDefBattleValue() { + auto& val = lcf::Data::system.easyrpg_max_stat_battle_value; + TryGetOverriddenConstant(EXE::Shared::GameConstantType::MaxDefBattleValue, val); + if (val == -1) { + return max_stat_battle_value; + } + return val; +} + +int32_t Player::Constants::MaxSpiBattleValue() { + auto& val = lcf::Data::system.easyrpg_max_stat_battle_value; + TryGetOverriddenConstant(EXE::Shared::GameConstantType::MaxSpiBattleValue, val); + if (val == -1) { + return max_stat_battle_value; + } + return val; +} + +int32_t Player::Constants::MaxAgiBattleValue() { + auto& val = lcf::Data::system.easyrpg_max_stat_battle_value; + TryGetOverriddenConstant(EXE::Shared::GameConstantType::MaxAgiBattleValue, val); + if (val == -1) { + return max_stat_battle_value; + } + return val; +} + +int32_t Player::Constants::MaxDamageValue() { + auto& val = lcf::Data::system.easyrpg_max_damage; + TryGetOverriddenConstant(EXE::Shared::GameConstantType::MaxDamageValue, val); + if (val == -1) { + return (Player::IsRPG2k() ? 999 : 9'999); + } + return val; +} + +int32_t Player::Constants::MaxExpValue() { + auto& val = lcf::Data::system.easyrpg_max_exp; + TryGetOverriddenConstant(EXE::Shared::GameConstantType::MaxExpValue, val); + if (val == -1) { + return Player::IsRPG2k() ? 999'999 : 9'999'999; + } + return val; +} + +int32_t Player::Constants::MaxLevel() { + int max_level = Player::IsRPG2k() ? max_level_2k : max_level_2k3; + if (TryGetOverriddenConstant(EXE::Shared::GameConstantType::MaxLevel, max_level)) { + return max_level; + } + if (lcf::Data::system.easyrpg_max_level > -1) { + max_level = lcf::Data::system.easyrpg_max_level; + } + return max_level; +} + +int32_t Player::Constants::MaxGoldValue() { + int32_t max_gold = 999'999; + if (TryGetOverriddenConstant(EXE::Shared::GameConstantType::MaxGoldValue, max_gold)) { + return max_gold; + } + return max_gold; +} + +int32_t Player::Constants::MaxItemCount() { + int32_t max_item_count = (lcf::Data::system.easyrpg_max_item_count == -1 ? 99 : lcf::Data::system.easyrpg_max_item_count);; + TryGetOverriddenConstant(EXE::Shared::GameConstantType::MaxItemCount, max_item_count); + return max_item_count; +} + +int32_t Player::Constants::MaxSaveFiles() { + int32_t max_savefiles = Utils::Clamp(lcf::Data::system.easyrpg_max_savefiles, 3, 99); + if (TryGetOverriddenConstant(EXE::Shared::GameConstantType::MaxSaveFiles, max_savefiles)) { + return max_savefiles - 1; + } + return max_savefiles; +} + +bool Player::Constants::TryGetOverriddenConstant(EXE::Shared::GameConstantType const_type, int32_t& out_value) { + auto it = constant_overrides.find(const_type); + if (it != constant_overrides.end()) { + out_value = (*it).second; + } + return it != constant_overrides.end(); +} + +void Player::Constants::OverrideGameConstant(EXE::Shared::GameConstantType const_type, int32_t value) { + constant_overrides[const_type] = value; +} + +void Player::Constants::ResetOverrides() { + constant_overrides.clear(); +} + +void Player::Constants::PrintActiveOverrides() { + std::vector> overridden_constants; + + auto tags = EXE::Shared::kGameConstantType.tags(); + int32_t value; + for (int i = 0; i < tags.size(); i++) { + auto const_type = static_cast(tags[i].value); + if (!TryGetOverriddenConstant(const_type, value)) { + continue; + } + + overridden_constants.push_back(std::make_tuple(tags[i].name, value)); + } + + if (!overridden_constants.empty()) { + std::string out = "Overridden Game Constants: "; + bool first = true; + for (const auto& p : overridden_constants) { + if (!first) { + out += ", "; + } + out += fmt::format("{}: {}", std::get(p), std::get(p)); + first = false; + } + Output::DebugStr(out); + } +} + +bool Player::Constants::HasEmbeddedTemplateString(EXE::Shared::EmbeddedStringTypes type, StringView& template_string) { + //TODO + + using Str = EXE::Shared::EmbeddedStringTypes; + + switch (type) { + case Str::Battle_DamageToEnemy: + case Str::Battle_DamageToAlly: + template_string = "%s took %d%s"; + return true; + default: + break; + } + + return false; +} + +void Player::Constants::SetTemplateString(EXE::Shared::EmbeddedStringTypes type, std::string template_string) { + auto apply_new_placeholders = [](std::string& str, std::initializer_list replacements) { + size_t index = str.find("%"); + auto repl = replacements.begin(); + while (index != std::string::npos) { + index++; + if (index < str.length()) { + char type = str[index]; + if (type == 's' && replacements.end() != repl) { + str[index] = *repl; + ++repl; + } else if (type == 'd') { + str[index] = 'V'; + } + } + + index = str.find("%", index); + } + }; + + using Str = EXE::Shared::EmbeddedStringTypes; + + switch (type) { + case Str::Battle_DamageToEnemy: + case Str::Battle_DamageToAlly: + apply_new_placeholders(template_string, { 'S', 'T' }); + break; + case Str::Battle_AbsorbEnemy: + case Str::Battle_AbsorbAlly: + apply_new_placeholders(template_string, { 'O', 'U', 'T'}); + break; + case Str::Battle_UseItem: + apply_new_placeholders(template_string, { 'S', 'O', 'T' }); + break; + case Str::Battle_StatIncrease: + case Str::Battle_StatDecrease: + case Str::Battle_HpSpRecovery: + apply_new_placeholders(template_string, { 'S', 'U', 'T'}); + break; + case Str::Msg_LevelUp: + apply_new_placeholders(template_string, { 'S', 'L', 'T' }); + break; + default: + break; + } + + //TODO +} diff --git a/src/player.h b/src/player.h index c8ab392c65..e3ef5c3166 100644 --- a/src/player.h +++ b/src/player.h @@ -19,12 +19,14 @@ #define EP_PLAYER_H // Headers +#include "exe_shared.h" #include "fileext_guesser.h" #include "meta.h" #include "translation.h" #include "game_clock.h" #include "game_config.h" #include "game_config_game.h" +#include "game_variables.h" #include #include #include @@ -130,6 +132,10 @@ namespace Player { */ void CreateGameObjects(); + void DetectEngine(bool ignore_patch_override); + + void PrintEngineInfo(); + /** * Change the resolution of the Player * @@ -297,6 +303,8 @@ namespace Player { */ bool IsPatchDestiny(); + bool HasBreakLoopFix(); + /** * @return True when EasyRpg extensions are on */ @@ -407,6 +415,8 @@ namespace Player { /** Translation manager, including list of languages and current translation. */ extern Translation translation; + extern bool break_loop_fix; + /** * The default speed modifier applied when the speed up button is pressed * Only used for configuring the speedup, don't read this var directly use @@ -427,6 +437,55 @@ namespace Player { /** Name of game emscripten uses */ extern std::string emscripten_game_name; #endif + + namespace Constants { + using Var_t = int32_t; + + static constexpr int32_t max_level_2k = 50; + static constexpr int32_t max_level_2k3 = 99; + + static constexpr int32_t max_hp_2k = 999; + static constexpr int32_t max_hp_2k3 = 9'999; + static constexpr int32_t max_stat_base_value = 999; + static constexpr int32_t max_stat_battle_value = 9'999; + + void GetVariableLimits(Var_t& min_var, Var_t& max_var); + + int32_t MaxActorHpValue(); + int32_t MaxActorSpValue(); + + int32_t MaxEnemyHpValue(); + int32_t MaxEnemySpValue(); + + int32_t MaxAtkBaseValue(); + int32_t MaxDefBaseValue(); + int32_t MaxSpiBaseValue(); + int32_t MaxAgiBaseValue(); + + int32_t MaxAtkBattleValue(); + int32_t MaxDefBattleValue(); + int32_t MaxSpiBattleValue(); + int32_t MaxAgiBattleValue(); + + int32_t MaxDamageValue(); + + int32_t MaxExpValue(); + int32_t MaxLevel(); + + int32_t MaxGoldValue(); + int32_t MaxItemCount(); + int32_t MaxSaveFiles(); + + extern std::map constant_overrides; + + bool TryGetOverriddenConstant(EXE::Shared::GameConstantType const_type, int32_t& out_value); + void OverrideGameConstant(EXE::Shared::GameConstantType const_type, int32_t value); + void ResetOverrides(); + void PrintActiveOverrides(); + + bool HasEmbeddedTemplateString(EXE::Shared::EmbeddedStringTypes type, StringView& template_string); + void SetTemplateString(EXE::Shared::EmbeddedStringTypes type, std::string template_string); + } } inline bool Player::IsRPG2k() { @@ -497,8 +556,11 @@ inline bool Player::IsPatchDestiny() { return game_config.patch_destiny.Get(); } +inline bool Player::HasBreakLoopFix() { + return IsPatchManiac() || break_loop_fix; +} + inline bool Player::HasEasyRpgExtensions() { return game_config.patch_easyrpg.Get(); } - #endif diff --git a/src/scene_equip.cpp b/src/scene_equip.cpp index a5390f2a60..6765041d23 100644 --- a/src/scene_equip.cpp +++ b/src/scene_equip.cpp @@ -135,12 +135,10 @@ void Scene_Equip::UpdateStatusWindow() { } add_item(current_item, 1); - int limit = actor.MaxStatBaseValue(); - - atk = Utils::Clamp(atk, 1, limit); - def = Utils::Clamp(def, 1, limit); - spi = Utils::Clamp(spi, 1, limit); - agi = Utils::Clamp(agi, 1, limit); + atk = Utils::Clamp(atk, 1, Player::Constants::MaxAtkBaseValue()); + def = Utils::Clamp(def, 1, Player::Constants::MaxDefBaseValue()); + spi = Utils::Clamp(spi, 1, Player::Constants::MaxSpiBaseValue()); + agi = Utils::Clamp(agi, 1, Player::Constants::MaxAgiBaseValue()); atk = actor.CalcValueAfterAtkStates(atk); def = actor.CalcValueAfterDefStates(def); diff --git a/src/scene_file.cpp b/src/scene_file.cpp index 4e23f64c9c..cd9fa341b9 100644 --- a/src/scene_file.cpp +++ b/src/scene_file.cpp @@ -123,7 +123,7 @@ void Scene_File::Start() { // Refresh File Finder Save Folder fs = FileFinder::Save(); - for (int i = 0; i < Utils::Clamp(lcf::Data::system.easyrpg_max_savefiles, 3, 99); i++) { + for (int i = 0; i < Player::Constants::MaxSaveFiles(); i++) { std::shared_ptr w(new Window_SaveFile(Player::menu_offset_x, 40 + i * 64, MENU_WIDTH, 64)); w->SetIndex(i); @@ -168,7 +168,7 @@ void Scene_File::RefreshWindows() { } void Scene_File::Refresh() { - for (int i = 0; i < Utils::Clamp(lcf::Data::system.easyrpg_max_savefiles, 3, 99); i++) { + for (int i = 0; i < Player::Constants::MaxSaveFiles(); i++) { Window_SaveFile *w = file_windows[i].get(); PopulateSaveWindow(*w, i); w->Refresh(); diff --git a/src/scene_save.cpp b/src/scene_save.cpp index c802b103ad..a01a54766a 100644 --- a/src/scene_save.cpp +++ b/src/scene_save.cpp @@ -53,7 +53,7 @@ Scene_Save::Scene_Save() : void Scene_Save::Start() { Scene_File::Start(); - for (int i = 0; i < Utils::Clamp(lcf::Data::system.easyrpg_max_savefiles, 3, 99); i++) { + for (int i = 0; i < Player::Constants::MaxSaveFiles(); i++) { file_windows[i]->SetHasSave(true); file_windows[i]->Refresh(); } diff --git a/src/scene_title.cpp b/src/scene_title.cpp index 4dc5280a58..f2b0f8846e 100644 --- a/src/scene_title.cpp +++ b/src/scene_title.cpp @@ -226,12 +226,29 @@ void Scene_Title::CreateTitleGraphic() { } void Scene_Title::RepositionWindow(Window_Command& window, bool center_vertical) { + int title_x = 160; if (!center_vertical) { - window.SetX(Player::screen_width / 2 - window.GetWidth() / 2); - window.SetY(Player::screen_height * 53 / 60 - window.GetHeight()); + int title_y = 148; + if (Player::Constants::TryGetOverriddenConstant(EXE::Shared::GameConstantType::TitleX, title_x) + || Player::Constants::TryGetOverriddenConstant(EXE::Shared::GameConstantType::TitleY, title_y)) { + // RPG_RT aligns its command window as "CenterTop" + window.SetX(title_x - window.GetWidth() / 2 + Player::menu_offset_x); + window.SetY(title_y + Player::menu_offset_y); + } else { + window.SetX(Player::screen_width / 2 - window.GetWidth() / 2); + window.SetY(Player::screen_height * 53 / 60 - window.GetHeight()); + } } else { - window.SetX(Player::screen_width / 2 - window.GetWidth() / 2); - window.SetY(Player::screen_height / 2 - window.GetHeight() / 2); + int title_y = 88; + if (Player::Constants::TryGetOverriddenConstant(EXE::Shared::GameConstantType::TitleHiddenX, title_x) + || Player::Constants::TryGetOverriddenConstant(EXE::Shared::GameConstantType::TitleHiddenY, title_y)) { + // RPG_RT aligns its command window as "CenterTop" + window.SetX(title_x - window.GetWidth() / 2 + Player::menu_offset_x); + window.SetY(title_y + Player::menu_offset_y); + } else { + window.SetX(Player::screen_width / 2 - window.GetWidth() / 2); + window.SetY(Player::screen_height / 2 - window.GetHeight() / 2); + } } } diff --git a/src/window_equipstatus.cpp b/src/window_equipstatus.cpp index 719fced295..b794159984 100644 --- a/src/window_equipstatus.cpp +++ b/src/window_equipstatus.cpp @@ -114,7 +114,9 @@ void Window_EquipStatus::DrawParameter(int cx, int cy, int type) { } // Check if 4 digits are needed instead of 3 - int limit = actor.MaxStatBaseValue(); + int limit = std::max(Player::Constants::MaxAtkBaseValue(), Player::Constants::MaxDefBaseValue()); + limit = std::max(limit, Player::Constants::MaxSpiBaseValue()); + limit = std::max(limit, Player::Constants::MaxAgiBaseValue()); bool more_space_needed = (Player::IsRPG2k3() && limit >= 500) || limit >= 1000; // Draw Term diff --git a/src/window_shopparty.cpp b/src/window_shopparty.cpp index d2b98e8ed2..f871756f1e 100644 --- a/src/window_shopparty.cpp +++ b/src/window_shopparty.cpp @@ -22,6 +22,7 @@ #include "game_actor.h" #include "window_shopparty.h" #include "output.h" +#include "player.h" #include #include "sprite_character.h" @@ -101,12 +102,10 @@ static int CmpEquip(const Game_Actor* actor, const lcf::rpg::Item* new_item) { } add_item(new_item, 1); - int limit = actor->MaxStatBaseValue(); - - atk = Utils::Clamp(atk, 1, limit); - def = Utils::Clamp(def, 1, limit); - spi = Utils::Clamp(spi, 1, limit); - agi = Utils::Clamp(agi, 1, limit); + atk = Utils::Clamp(atk, 1, Player::Constants::MaxAtkBaseValue()); + def = Utils::Clamp(def, 1, Player::Constants::MaxDefBaseValue()); + spi = Utils::Clamp(spi, 1, Player::Constants::MaxSpiBaseValue()); + agi = Utils::Clamp(agi, 1, Player::Constants::MaxAgiBaseValue()); int new_score = atk + def + spi + agi; diff --git a/tests/game_actor.cpp b/tests/game_actor.cpp index 52ac73a3f9..efc40e8db2 100644 --- a/tests/game_actor.cpp +++ b/tests/game_actor.cpp @@ -113,8 +113,6 @@ static void testLimits(int hp, int base, int battle) { auto actor = MakeActor(1, 1, 99, 1, 1, 1, 1, 1, 1); REQUIRE_EQ(actor.MaxHpValue(), hp); - REQUIRE_EQ(actor.MaxStatBaseValue(), base); - REQUIRE_EQ(actor.MaxStatBattleValue(), battle); } SUBCASE("base limits") { diff --git a/tests/game_enemy.cpp b/tests/game_enemy.cpp index 0044dfe57e..f97bb22ffb 100644 --- a/tests/game_enemy.cpp +++ b/tests/game_enemy.cpp @@ -56,8 +56,6 @@ static void testLimits(int hp, int base, int battle) { auto& enemy = MakeEnemy(1, 100, 10, 11, 12, 13, 14); REQUIRE_EQ(enemy.MaxHpValue(), hp); - REQUIRE_EQ(enemy.MaxStatBaseValue(), base); - REQUIRE_EQ(enemy.MaxStatBattleValue(), battle); SUBCASE("up") { enemy.SetAtkModifier(999999);