diff --git a/src/game_interpreter.cpp b/src/game_interpreter.cpp index e3b9dd0b41..c0242709d8 100644 --- a/src/game_interpreter.cpp +++ b/src/game_interpreter.cpp @@ -977,7 +977,7 @@ bool Game_Interpreter::CommandChangeFaceGraphic(lcf::rpg::EventCommand const& co return false; } - Main_Data::game_system->SetMessageFaceName(ToString(CommandStringOrVariableBitfield(com, 3, 0, 4))); + Main_Data::game_system->SetMessageFaceName(CommandStringOrVariableBitfield(com, 3, 0, 4)); Main_Data::game_system->SetMessageFaceIndex(ValueOrVariableBitfield(com, 3, 1, 0)); Main_Data::game_system->SetMessageFaceRightPosition(com.parameters[1] != 0); Main_Data::game_system->SetMessageFaceFlipped(com.parameters[2] != 0); @@ -1785,33 +1785,47 @@ int Game_Interpreter::ValueOrVariableBitfield(lcf::rpg::EventCommand const& com, return com.parameters[val_idx]; } -StringView Game_Interpreter::CommandStringOrVariable(lcf::rpg::EventCommand const& com, int mode_idx, int val_idx) { +std::string Game_Interpreter::CommandString(lcf::rpg::EventCommand const& com) { +#ifdef LIBLCF_STUB_COMSTRING_VARSUBSTITUTION + if (!Player::HasEasyRpgExtensions()) { +#else + if (!Player::HasEasyRpgExtensions() || !lcf::Data::system.easyrpg_var_substitution_in_commands) { +#endif + return ToString(com.string); + } + std::string command_string = ToString(com.string); + PendingMessage::ApplyTextInsertingCommands(command_string, Player::escape_char, Game_Message::CommandCodeInserter); + return command_string; +} + + +std::string Game_Interpreter::CommandStringOrVariable(lcf::rpg::EventCommand const& com, int mode_idx, int val_idx) { if (!Player::IsPatchManiac()) { - return com.string; + return CommandString(com); } assert(mode_idx != val_idx); if (com.parameters.size() > std::max(mode_idx, val_idx)) { - return Main_Data::game_strings->GetWithMode(ToString(com.string), com.parameters[mode_idx], com.parameters[val_idx], *Main_Data::game_variables); + return ToString(Main_Data::game_strings->GetWithMode(ToString(com.string), com.parameters[mode_idx], com.parameters[val_idx], *Main_Data::game_variables)); } - return com.string; + return CommandString(com); } -StringView Game_Interpreter::CommandStringOrVariableBitfield(lcf::rpg::EventCommand const& com, int mode_idx, int shift, int val_idx) { +std::string Game_Interpreter::CommandStringOrVariableBitfield(lcf::rpg::EventCommand const& com, int mode_idx, int shift, int val_idx) { if (!Player::IsPatchManiac()) { - return com.string; + return CommandString(com); } assert(mode_idx != val_idx); if (com.parameters.size() >= std::max(mode_idx, val_idx) + 1) { int mode = com.parameters[mode_idx]; - return Main_Data::game_strings->GetWithMode(ToString(com.string), (mode & (0xF << shift * 4)) >> shift * 4, com.parameters[val_idx], *Main_Data::game_variables); + return ToString(Main_Data::game_strings->GetWithMode(ToString(com.string), (mode & (0xF << shift * 4)) >> shift * 4, com.parameters[val_idx], *Main_Data::game_variables)); } - return com.string; + return CommandString(com); } bool Game_Interpreter::CommandChangeParameters(lcf::rpg::EventCommand const& com) { // Code 10430 @@ -2088,7 +2102,7 @@ bool Game_Interpreter::CommandWait(lcf::rpg::EventCommand const& com) { // code bool Game_Interpreter::CommandPlayBGM(lcf::rpg::EventCommand const& com) { // code 11510 lcf::rpg::Music music; - music.name = ToString(CommandStringOrVariableBitfield(com, 4, 0, 5)); + music.name = CommandStringOrVariableBitfield(com, 4, 0, 5); music.fadein = ValueOrVariableBitfield(com, 4, 1, 0); music.volume = ValueOrVariableBitfield(com, 4, 2, 1); @@ -2107,7 +2121,7 @@ bool Game_Interpreter::CommandFadeOutBGM(lcf::rpg::EventCommand const& com) { // bool Game_Interpreter::CommandPlaySound(lcf::rpg::EventCommand const& com) { // code 11550 lcf::rpg::Sound sound; - sound.name = ToString(CommandStringOrVariableBitfield(com, 3, 0, 4)); + sound.name = CommandStringOrVariableBitfield(com, 3, 0, 4); sound.volume = ValueOrVariableBitfield(com, 3, 1, 0); sound.tempo = ValueOrVariableBitfield(com, 3, 2, 1); @@ -2179,7 +2193,7 @@ bool Game_Interpreter::CommandChangeHeroName(lcf::rpg::EventCommand const& com) return true; } - actor->SetName(ToString(CommandStringOrVariableBitfield(com, 1, 1, 2))); + actor->SetName(CommandStringOrVariableBitfield(com, 1, 1, 2)); return true; } @@ -2192,7 +2206,7 @@ bool Game_Interpreter::CommandChangeHeroTitle(lcf::rpg::EventCommand const& com) return true; } - actor->SetTitle(ToString(CommandStringOrVariableBitfield(com, 1, 1, 2))); + actor->SetTitle(CommandStringOrVariableBitfield(com, 1, 1, 2)); return true; } @@ -2205,7 +2219,7 @@ bool Game_Interpreter::CommandChangeSpriteAssociation(lcf::rpg::EventCommand con return true; } - auto file = ToString(CommandStringOrVariableBitfield(com, 3, 1, 4)); + auto file = CommandStringOrVariableBitfield(com, 3, 1, 4); int idx = ValueOrVariableBitfield(com, 3, 2, 1); bool transparent = com.parameters[2] != 0; actor->SetSprite(file, idx, transparent); @@ -2223,7 +2237,7 @@ bool Game_Interpreter::CommandChangeActorFace(lcf::rpg::EventCommand const& com) } actor->SetFace( - ToString(CommandStringOrVariableBitfield(com, 2, 1, 3)), + CommandStringOrVariableBitfield(com, 2, 1, 3), ValueOrVariableBitfield(com, 2, 2, 1)); return true; } @@ -2237,7 +2251,7 @@ bool Game_Interpreter::CommandChangeVehicleGraphic(lcf::rpg::EventCommand const& return true; } - const std::string& name = ToString(com.string); + std::string name = CommandString(com); int vehicle_index = com.parameters[1]; vehicle->SetSpriteGraphic(name, vehicle_index); @@ -2249,7 +2263,7 @@ bool Game_Interpreter::CommandChangeVehicleGraphic(lcf::rpg::EventCommand const& bool Game_Interpreter::CommandChangeSystemBGM(lcf::rpg::EventCommand const& com) { //code 10660 lcf::rpg::Music music; int context = com.parameters[0]; - music.name = ToString(com.string); + music.name = CommandString(com); music.fadein = com.parameters[1]; music.volume = com.parameters[2]; music.tempo = com.parameters[3]; @@ -2261,7 +2275,7 @@ bool Game_Interpreter::CommandChangeSystemBGM(lcf::rpg::EventCommand const& com) bool Game_Interpreter::CommandChangeSystemSFX(lcf::rpg::EventCommand const& com) { //code 10670 lcf::rpg::Sound sound; int context = com.parameters[0]; - sound.name = ToString(com.string); + sound.name = CommandString(com); sound.volume = com.parameters[1]; sound.tempo = com.parameters[2]; sound.balance = com.parameters[3]; @@ -2270,7 +2284,7 @@ bool Game_Interpreter::CommandChangeSystemSFX(lcf::rpg::EventCommand const& com) } bool Game_Interpreter::CommandChangeSystemGraphics(lcf::rpg::EventCommand const& com) { // code 10680 - Main_Data::game_system->SetSystemGraphic(ToString(CommandStringOrVariable(com, 2, 3)), + Main_Data::game_system->SetSystemGraphic(CommandStringOrVariable(com, 2, 3), static_cast(com.parameters[0]), static_cast(com.parameters[1])); @@ -2796,7 +2810,7 @@ bool Game_Interpreter::CommandShowPicture(lcf::rpg::EventCommand const& com) { / int pic_id = com.parameters[0]; Game_Pictures::ShowParams params = {}; - params.name = ToString(com.string); + params.name = CommandString(com); // Maniac Patch uses the upper bits for X/Y origin, mask it away int pos_mode = ManiacBitmask(com.parameters[1], 0xFF); params.position_x = ValueOrVariable(pos_mode, com.parameters[2]); @@ -2830,7 +2844,7 @@ bool Game_Interpreter::CommandShowPicture(lcf::rpg::EventCommand const& com) { / // Handling of RPG2k3 1.12 chunks if (Player::IsPatchManiac()) { pic_id = ValueOrVariableBitfield(com.parameters[17], 0, pic_id); - params.name = ToString(CommandStringOrVariableBitfield(com, 17, 2, 30)); + params.name = CommandStringOrVariableBitfield(com, 17, 2, 30); } else { pic_id = ValueOrVariable(com.parameters[17], pic_id); } @@ -3390,7 +3404,7 @@ bool Game_Interpreter::CommandChangeMapTileset(lcf::rpg::EventCommand const& com bool Game_Interpreter::CommandChangePBG(lcf::rpg::EventCommand const& com) { // code 11720 Game_Map::Parallax::Params params; - params.name = ToString(com.string); + params.name = CommandString(com); params.scroll_horz = com.parameters[0] != 0; params.scroll_vert = com.parameters[1] != 0; params.scroll_horz_auto = com.parameters[2] != 0; @@ -3552,9 +3566,12 @@ bool Game_Interpreter::CommandConditionalBranch(lcf::rpg::EventCommand const& co result = Main_Data::game_party->IsActorInParty(actor_id); break; case 1: + { // Name - result = (actor->GetName() == com.string); + std::string name = CommandString(com); + result = (actor->GetName() == name); break; + } case 2: // Higher or equal level result = (actor->GetLevel() >= com.parameters[3]); diff --git a/src/game_interpreter.h b/src/game_interpreter.h index df5f4c4e47..d7e83ff4c3 100644 --- a/src/game_interpreter.h +++ b/src/game_interpreter.h @@ -172,8 +172,11 @@ class Game_Interpreter static int ValueOrVariableBitfield(int mode, int shift, int val); // Range checked, conditional version (slower) of ValueOrVariableBitfield static int ValueOrVariableBitfield(lcf::rpg::EventCommand const& com, int mode_idx, int shift, int val_idx); - static StringView CommandStringOrVariable(lcf::rpg::EventCommand const& com, int mode_idx, int val_idx); - static StringView CommandStringOrVariableBitfield(lcf::rpg::EventCommand const& com, int mode_idx, int shift, int val_idx); + + static std::string CommandString(lcf::rpg::EventCommand const& com); + static std::string CommandStringOrVariable(lcf::rpg::EventCommand const& com, int mode_idx, int val_idx); + static std::string CommandStringOrVariableBitfield(lcf::rpg::EventCommand const& com, int mode_idx, int shift, int val_idx); + /** * When current frame finishes executing we pop the stack diff --git a/src/game_interpreter_battle.cpp b/src/game_interpreter_battle.cpp index 7fba49a61d..98ff487cf8 100644 --- a/src/game_interpreter_battle.cpp +++ b/src/game_interpreter_battle.cpp @@ -421,7 +421,7 @@ bool Game_Interpreter_Battle::CommandShowHiddenMonster(lcf::rpg::EventCommand co } bool Game_Interpreter_Battle::CommandChangeBattleBG(lcf::rpg::EventCommand const& com) { - Game_Battle::ChangeBackground(ToString(com.string)); + Game_Battle::ChangeBackground(CommandString(com)); return true; } diff --git a/src/game_interpreter_map.cpp b/src/game_interpreter_map.cpp index 5bf55e4353..7a66efc288 100644 --- a/src/game_interpreter_map.cpp +++ b/src/game_interpreter_map.cpp @@ -283,7 +283,7 @@ bool Game_Interpreter_Map::CommandEnemyEncounter(lcf::rpg::EventCommand const& c Game_Map::SetupBattle(args); break; case 1: - args.background = ToString(com.string); + args.background = CommandString(com); if (Player::IsRPG2k3()) { args.formation = static_cast(com.parameters[7]); @@ -699,7 +699,7 @@ bool Game_Interpreter_Map::CommandPlayMovie(lcf::rpg::EventCommand const& com) { return false; } - auto filename = ToString(com.string); + auto filename = CommandString(com); int pos_x = ValueOrVariable(com.parameters[0], com.parameters[1]); int pos_y = ValueOrVariable(com.parameters[0], com.parameters[2]); int res_x = com.parameters[3]; diff --git a/src/game_message.cpp b/src/game_message.cpp index cfb9edd653..2f986d23c6 100644 --- a/src/game_message.cpp +++ b/src/game_message.cpp @@ -164,7 +164,8 @@ static std::optional CommandCodeInserterNoRecurse(char ch, const ch std::string str = ToString(Main_Data::game_strings->Get(value)); // \t[] is evaluated but command codes inside it are not evaluated again - return PendingMessage::ApplyTextInsertingCommands(str, escape_char, PendingMessage::DefaultCommandInserter); + PendingMessage::ApplyTextInsertingCommands(str, escape_char, PendingMessage::DefaultCommandInserter); + return str; } return PendingMessage::DefaultCommandInserter(ch, iter, end, escape_char); @@ -179,112 +180,247 @@ std::optional Game_Message::CommandCodeInserter(char ch, const char std::string str = ToString(Main_Data::game_strings->Get(value)); // Command codes in \t[] are evaluated once. - return PendingMessage::ApplyTextInsertingCommands(str, escape_char, CommandCodeInserterNoRecurse); + PendingMessage::ApplyTextInsertingCommands(str, escape_char, CommandCodeInserterNoRecurse); + return str; } return PendingMessage::DefaultCommandInserter(ch, iter, end, escape_char); } -Game_Message::ParseParamResult Game_Message::ParseParam( - char upper, - char lower, - const char* iter, - const char* end, - uint32_t escape_char, - bool skip_prefix, - int max_recursion) -{ - if (!skip_prefix) { - const auto begin = iter; - if (iter == end) { - return { begin, 0 }; - } - auto ret = Utils::UTF8Next(iter, end); - // Invalid commands - if (ret.ch != escape_char) { - return { begin, 0 }; - } - iter = ret.next; - if (iter == end || (*iter != upper && *iter != lower)) { +namespace { + template + inline R DefaultEmpty(const char* begin) { + if constexpr (std::is_same::value) { return { begin, 0 }; + } else if constexpr (std::is_same::value) { + return { begin, "" }; + } else { + return { begin, 0, "" }; } - ++iter; } - // If no bracket, RPG_RT will return 0. - if (iter == end || *iter != '[') { - return { iter, 0 }; + // Note: this function doesn't consider any floating types or precision arguments as those are relevant for EasyRPG + // If used, the format string is considered invalid + inline const char* ParseNumberFormat(const char* iter, const char* end) { + int step = 0; + + while (iter != end && *iter != ']') { + auto ch = *iter; + + switch (step) { + case 0: //expect 'fill' character + if (!(ch == '{' || ch == '}')) { + ++iter; + if (iter == end || *iter == ']') { + --iter; + step++; + continue; + } + ch = *iter; + //expect 'align' + if (!(ch == '<' || ch == '>' || ch == '^')) { + step++; + --iter; + continue; + } + step += 2; + } + break; + case 1: //expect 'align' + if (!(ch == '<' || ch == '>' || ch == '^')) { + step++; + continue; + } + break; + case 2: //expect 'sign' + if (!(ch == '+' || ch == '-' || ch == ' ')) { + step++; + continue; + } + break; + case 3: //expect '#' + if (ch != '#') { + step++; + continue; + } + break; + case 4: //expect '0' + if (ch != '0') { + step++; + continue; + } + case 5: //expect number 'width' + // Fast inline isdigit() + if (ch >= '0' && ch <= '9') { + ++iter; + continue; + } else { + step++; + continue; + } + break; + case 6: //expect 'type' + if (!(ch == 'b' || ch == 'B' || ch == 'c' || ch == 'C' || ch == 'd' || ch == 'D' || ch == 'o' || ch == 'O' || ch == 'x' || ch == 'X')) + return end; + break; + } + ++iter; + } + return iter; } - int value = 0; - ++iter; - bool stop_parsing = false; - bool got_valid_number = false; + template + R ParseParamImpl(char upper, + char lower, + const char* iter, + const char* end, + uint32_t escape_char, + bool skip_prefix, + int max_recursion) { - while (iter != end && *iter != ']') { - if (stop_parsing) { + if (!skip_prefix) { + const auto begin = iter; + if (iter == end) { + return DefaultEmpty(begin); + } + auto ret = Utils::UTF8Next(iter, end); + // Invalid commands + if (ret.ch != escape_char) { + return DefaultEmpty(begin); + } + iter = ret.next; + if (iter == end || (*iter != upper && *iter != lower)) { + return DefaultEmpty(begin); + } ++iter; - continue; } - // Fast inline isdigit() - if (*iter >= '0' && *iter <= '9') { - value *= 10; - value += (*iter - '0'); - ++iter; - got_valid_number = true; - continue; + if (iter == end || *iter != '[') { + // If no bracket, RPG_RT will return 0. + return DefaultEmpty(iter); } - if (max_recursion > 0) { - auto ret = Utils::UTF8Next(iter, end); - auto ch = ret.ch; - iter = ret.next; + V value; + if constexpr (std::is_same::value) { + value = 0; + } else if constexpr (std::is_same::value) { + value = ""; + } - // Recursive variable case. - if (ch == escape_char) { - if (iter != end && (*iter == 'V' || *iter == 'v')) { - ++iter; + ++iter; + bool stop_parsing = false; + bool got_valid_number = false; - auto ret = ParseParam('V', 'v', iter, end, escape_char, true, max_recursion - 1); - iter = ret.next; - int var_val = Main_Data::game_variables->Get(ret.value); + while (iter != end && *iter != ']') { + if (stop_parsing) { + ++iter; + continue; + } + if constexpr (std::is_same::value) { + // Fast inline isdigit() + if (*iter >= '0' && *iter <= '9') { + value *= 10; + value += (*iter - '0'); + ++iter; got_valid_number = true; + continue; + } + } else if constexpr (std::is_same::value) { + value += *iter; + } - // RPG_RT concatenates the variable value. - int m = 10; - if (value != 0) { - while (m < var_val) { - m *= 10; + if constexpr (std::is_same::value) { + if (upper == 'V' && *iter == ':' && got_valid_number) { + ++iter; + // validate, if everything after ':' is according to fmt specifications + // (see https://fmt.dev/latest/syntax.html) + const char* fmt_end = ParseNumberFormat(iter, end); + if (fmt_end != end && (fmt_end - iter) > 0) { + int l = (fmt_end - iter); + + char* number_format = new char[l + 4]; + number_format[0] = '{'; + number_format[1] = ':'; + for (int i = 0; i < l; i++) + number_format[i + 2] = *iter++; + number_format[l + 2] = '}'; + number_format[l + 3] = '\0'; + auto result = std::string(number_format); + delete[] number_format; + + iter = fmt_end; + if (iter != end) { + ++iter; } + return { iter, value, result }; } - value = value * m + var_val; continue; } } + + if (max_recursion > 0) { + auto ret = Utils::UTF8Next(iter, end); + auto ch = ret.ch; + iter = ret.next; + + // Recursive variable case. + if (ch == escape_char) { + if (iter != end && (*iter == 'V' || *iter == 'v')) { + ++iter; + + auto ret = Game_Message::ParseParam('V', 'v', iter, end, escape_char, true, max_recursion - 1); + iter = ret.next; + int var_val = Main_Data::game_variables->Get(ret.value); + + if constexpr (std::is_same::value) { + got_valid_number = true; + + // RPG_RT concatenates the variable value. + int m = 10; + if (value != 0) { + while (m < var_val) { + m *= 10; + } + } + value = value * m + var_val; + } else if constexpr (std::is_same::value) { + value += std::to_string(var_val); + } + continue; + } + } + } + + if constexpr (std::is_same::value) { + // If we hit a non-digit, RPG_RT will stop parsing until the next closing bracket. + stop_parsing = true; + } } - // If we hit a non-digit, RPG_RT will stop parsing until the next closing bracket. - stop_parsing = true; - } + if (iter != end) { + ++iter; + } - if (iter != end) { - ++iter; - } + if constexpr (std::is_same::value) { + // Actor 0 references the first party member + if (upper == 'N' && value == 0 && got_valid_number) { + auto* party = Main_Data::game_party.get(); + if (party->GetBattlerCount() > 0) { + value = (*party)[0].GetId(); + } + } + } - // Actor 0 references the first party member - if (upper == 'N' && value == 0 && got_valid_number) { - auto* party = Main_Data::game_party.get(); - if (party->GetBattlerCount() > 0) { - value = (*party)[0].GetId(); + if constexpr (std::is_same::value) { + return { iter, value, "" }; } - } - return { iter, value }; + return { iter, value }; + } } -Game_Message::ParseParamStringResult Game_Message::ParseStringParam( +Game_Message::ParseParamResult Game_Message::ParseParam( char upper, char lower, const char* iter, @@ -293,67 +429,41 @@ Game_Message::ParseParamStringResult Game_Message::ParseStringParam( bool skip_prefix, int max_recursion) { - if (!skip_prefix) { - const auto begin = iter; - if (iter == end) { - return { begin, "" }; - } - auto ret = Utils::UTF8Next(iter, end); - // Invalid commands - if (ret.ch != escape_char) { - return { begin, "" }; - } - iter = ret.next; - if (iter == end || (*iter != upper && *iter != lower)) { - return { begin, "" }; - } - ++iter; - } - - // If no bracket, return empty string - if (iter == end || *iter != '[') { - return { iter, "" }; - } - - std::string value; - ++iter; - - while (iter != end && *iter != ']') { - // Fast inline isdigit() - value += *iter; - - if (max_recursion > 0) { - auto ret = Utils::UTF8Next(iter, end); - auto ch = ret.ch; - iter = ret.next; - - // Recursive variable case. - if (ch == escape_char) { - if (iter != end && (*iter == 'V' || *iter == 'v')) { - ++iter; - - auto ret = ParseParam('V', 'v', iter, end, escape_char, true, max_recursion - 1); - iter = ret.next; - int var_val = Main_Data::game_variables->Get(ret.value); - - value += std::to_string(var_val); - continue; - } - } - } - } + return ParseParamImpl(upper, lower, iter, end, escape_char, skip_prefix, max_recursion); +} - if (iter != end) { - ++iter; - } +Game_Message::ParseParamStringResult Game_Message::ParseStringParam( + char upper, + char lower, + const char* iter, + const char* end, + uint32_t escape_char, + bool skip_prefix, + int max_recursion) +{ + return ParseParamImpl(upper, lower, iter, end, escape_char, skip_prefix, max_recursion); +} - return { iter, value }; +Game_Message::ParseFormattedParamResult Game_Message::ParseFormattedParam( + char upper, + char lower, + const char* iter, + const char* end, + uint32_t escape_char, + bool skip_prefix, + int max_recursion) +{ + return ParseParamImpl(upper, lower, iter, end, escape_char, skip_prefix, max_recursion); } Game_Message::ParseParamResult Game_Message::ParseVariable(const char* iter, const char* end, uint32_t escape_char, bool skip_prefix, int max_recursion) { return ParseParam('V', 'v', iter, end, escape_char, skip_prefix, max_recursion - 1); } +Game_Message::ParseFormattedParamResult Game_Message::ParseFormattedVariable(const char* iter, const char* end, uint32_t escape_char, bool skip_prefix, int max_recursion) { + return ParseFormattedParam('V', 'v', iter, end, escape_char, skip_prefix, max_recursion - 1); +} + Game_Message::ParseParamResult Game_Message::ParseString(const char* iter, const char* end, uint32_t escape_char, bool skip_prefix, int max_recursion) { return ParseParam('T', 't', iter, end, escape_char, skip_prefix, max_recursion); } diff --git a/src/game_message.h b/src/game_message.h index 555c2bdd21..474d4fbaff 100644 --- a/src/game_message.h +++ b/src/game_message.h @@ -122,6 +122,16 @@ namespace Game_Message { std::string value; }; + /** Struct returned by parameter parsing methods */ + struct ParseFormattedParamResult { + /** iterator to the next character after parsed content */ + const char* next = nullptr; + /** value that was parsed */ + int value = 0; + /** the format string according to fmt::format specifications */ + std::string format_string; + }; + /** Parse a \v[] variable string * * @param iter start of utf8 string @@ -134,6 +144,18 @@ namespace Game_Message { */ ParseParamResult ParseVariable(const char* iter, const char* end, uint32_t escape_char, bool skip_prefix = false, int max_recursion = default_max_recursion); + /** Parse a \v[] variable string with format specifiers + * + * @param iter start of utf8 string + * @param end end of utf8 string + * @param escape_char the escape character to use + * @param skip_prefix if true, assume prefix was already parsed and iter starts at the first left bracket. + * @param max_recursion How many times to allow recursive variable lookups. + * + * @return \refer ParseFormattedParamResult + */ + ParseFormattedParamResult ParseFormattedVariable(const char* iter, const char* end, uint32_t escape_char, bool skip_prefix = false, int max_recursion = default_max_recursion); + /** Parse a \t[] variable string * * @param iter start of utf8 string @@ -185,6 +207,10 @@ namespace Game_Message { Game_Message::ParseParamResult ParseParam(char upper, char lower, const char* iter, const char* end, uint32_t escape_char, bool skip_prefix = false, int max_recursion = default_max_recursion); // same as ParseParam but the parameter is of structure \x[some_word] instead of \x[1] Game_Message::ParseParamStringResult ParseStringParam(char upper, char lower, const char* iter, const char* end, uint32_t escape_char, bool skip_prefix = false, int max_recursion = default_max_recursion); + + Game_Message::ParseFormattedParamResult ParseFormattedParam(char upper, char lower, const char* iter, const char* end, uint32_t escape_char, bool skip_prefix = false, int max_recursion = default_max_recursion); + + const char* ParseNumberFormat(const char* iter, const char* end); } #endif diff --git a/src/game_strings.cpp b/src/game_strings.cpp index 1258a854ea..5876ab8d01 100644 --- a/src/game_strings.cpp +++ b/src/game_strings.cpp @@ -322,7 +322,9 @@ std::string Game_Strings::Extract(StringView string, bool as_hex) { cmd_fn = ManiacsCommandInserter; } - return PendingMessage::ApplyTextInsertingCommands(ToString(string), Player::escape_char, cmd_fn); + std::string str = ToString(string); + PendingMessage::ApplyTextInsertingCommands(str, Player::escape_char, cmd_fn); + return str; } std::optional Game_Strings::ManiacsCommandInserter(char ch, const char** iter, const char* end, uint32_t escape_char) { diff --git a/src/lcf/data.h b/src/lcf/data.h index 88806947ff..a2c9ef9801 100644 --- a/src/lcf/data.h +++ b/src/lcf/data.h @@ -34,6 +34,8 @@ #include "lcf/rpg/treemap.h" #include "lcf/rpg/database.h" +#define LIBLCF_STUB_COMSTRING_VARSUBSTITUTION + namespace lcf { /** diff --git a/src/pending_message.cpp b/src/pending_message.cpp index aea1fba180..cde353f480 100644 --- a/src/pending_message.cpp +++ b/src/pending_message.cpp @@ -44,7 +44,7 @@ PendingMessage::PendingMessage(PendingMessage::CommandInserter cmd_fn) : int PendingMessage::PushLineImpl(std::string msg) { RemoveControlChars(msg); - msg = ApplyTextInsertingCommands(std::move(msg), Player::escape_char, command_inserter); + ApplyTextInsertingCommands(msg, Player::escape_char, command_inserter); texts.push_back(std::move(msg)); return texts.size(); } @@ -94,17 +94,15 @@ void PendingMessage::SetChoiceResetColors(bool value) { choice_reset_color = value; } -std::string PendingMessage::ApplyTextInsertingCommands(std::string input, uint32_t escape_char, const CommandInserter& cmd_fn) { +void PendingMessage::ApplyTextInsertingCommands(std::string& input, uint32_t escape_char, const CommandInserter& cmd_fn) { if (input.empty()) { - return input; + return; } - std::string output; - const char* iter = input.data(); - const auto end = input.data() + input.size(); + auto end = input.data() + input.size(); - const char* start_copy = iter; + const char* start_replace = nullptr; while (iter != end) { auto ret = Utils::UTF8Next(iter, end); if (ret.ch != escape_char) { @@ -117,8 +115,7 @@ std::string PendingMessage::ApplyTextInsertingCommands(std::string input, uint32 break; } - output.append(start_copy, iter - start_copy); - start_copy = iter; + start_replace = iter; iter = ret.next; if (iter == end) { @@ -130,19 +127,17 @@ std::string PendingMessage::ApplyTextInsertingCommands(std::string input, uint32 auto fn_res = cmd_fn(ch, &iter, end, escape_char); if (fn_res) { - output.append(*fn_res); - start_copy = iter; - } - } + size_t repl_pos = start_replace - input.data(); + size_t repl_len = iter - start_replace; + size_t insert_len = fn_res->size(); - if (start_copy == input.data()) { - // Fast path - no substitutions occured, so just move the input into the return value. - output = std::move(input); - } else { - output.append(start_copy, end - start_copy); - } + input.replace(repl_pos, repl_len, fn_res->data(), insert_len); - return output; + iter = input.data() + repl_pos + insert_len; + end = input.data() + input.size(); + start_replace = nullptr; + } + } } std::optional PendingMessage::DefaultCommandInserter(char ch, const char** iter, const char* end, uint32_t escape_char) { @@ -159,12 +154,29 @@ std::optional PendingMessage::DefaultCommandInserter(char ch, const return ToString(actor->GetName()); } } else if (ch == 'V' || ch == 'v') { - auto parse_ret = Game_Message::ParseVariable(*iter, end, escape_char, true); - *iter = parse_ret.next; - int value = parse_ret.value; +#ifdef LIBLCF_STUB_COMSTRING_VARSUBSTITUTION + if (!Player::HasEasyRpgExtensions()) { +#else + if (!Player::HasEasyRpgExtensions() || !lcf::Data::system.easyrpg_var_substitution_formatting) { +#endif + auto parse_ret = Game_Message::ParseVariable(*iter, end, escape_char, true); + *iter = parse_ret.next; + int value = parse_ret.value; + + int variable_value = Main_Data::game_variables->Get(value); + return std::to_string(variable_value); + } else { + auto parse_ret = Game_Message::ParseFormattedVariable(*iter, end, escape_char, true); + *iter = parse_ret.next; + int value = parse_ret.value; + int variable_value = Main_Data::game_variables->Get(value); - int variable_value = Main_Data::game_variables->Get(value); - return std::to_string(variable_value); + if (!parse_ret.format_string.empty()) { + return fmt::format(parse_ret.format_string, variable_value); + } + + return std::to_string(variable_value); + } } return std::nullopt; diff --git a/src/pending_message.h b/src/pending_message.h index 793d47e498..3e61542810 100644 --- a/src/pending_message.h +++ b/src/pending_message.h @@ -68,7 +68,7 @@ class PendingMessage { void SetIsEventMessage(bool value) { is_event_message = value; } bool IsEventMessage() const { return is_event_message; } - static std::string ApplyTextInsertingCommands(std::string input, uint32_t escape_char, const CommandInserter& cmd_fn); + static void ApplyTextInsertingCommands(std::string& input, uint32_t escape_char, const CommandInserter& cmd_fn); private: int PushLineImpl(std::string msg);