From e44f42d47d72b766878c5e0c0f2c76846059df56 Mon Sep 17 00:00:00 2001 From: Mauro Junior <45118493+jetrotal@users.noreply.github.com> Date: Fri, 10 Oct 2025 17:22:45 -0300 Subject: [PATCH 1/4] maniacs patch - Remove colors upper limit more than 20 color are no possible. --- src/game_windows.cpp | 8 +++++++- src/window_message.cpp | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/game_windows.cpp b/src/game_windows.cpp index 8cd753e008..b95974d07d 100644 --- a/src/game_windows.cpp +++ b/src/game_windows.cpp @@ -436,7 +436,13 @@ void Game_Windows::Window_User::Refresh(bool& async_wait) { auto pres = Game_Message::ParseColor(text_index, end, Player::escape_char, true); auto value = pres.value; text_index = pres.next; - text_color = value > 19 ? 0 : value; + + if (Player::IsPatchManiac()) { + text_color = value; + } + else { + text_color = value > 19 ? 0 : value; + } } break; } diff --git a/src/window_message.cpp b/src/window_message.cpp index 4d797a29de..b98eb6f8a7 100644 --- a/src/window_message.cpp +++ b/src/window_message.cpp @@ -581,7 +581,13 @@ void Window_Message::UpdateMessage() { text_index = pres.next; DebugLogText("{}: MSG Color \\c[{}]", value); SetWaitForNonPrintable(0); - text_color = value > 19 ? 0 : value; + + if (Player::IsPatchManiac()) { + text_color = value; + } + else { + text_color = value > 19 ? 0 : value; + } } break; case 's': From dfe80b1049a935f187629823932a8083944c33a1 Mon Sep 17 00:00:00 2001 From: Mauro Junior <45118493+jetrotal@users.noreply.github.com> Date: Fri, 10 Oct 2025 18:19:01 -0300 Subject: [PATCH 2/4] Maniacs Patch - Parsing Arrays + x,y color and exfont picker --- src/cache.cpp | 2 ++ src/font.cpp | 32 +++++++++++++----- src/game_message.cpp | 77 ++++++++++++++++++++++++++++++++++++++++++ src/game_message.h | 11 ++++++ src/game_windows.cpp | 34 ++++++++++++------- src/utils.cpp | 64 ++++++++++++++++++++++++++++++----- src/utils.h | 4 ++- src/window_message.cpp | 35 ++++++++++++------- 8 files changed, 217 insertions(+), 42 deletions(-) diff --git a/src/cache.cpp b/src/cache.cpp index 6852bb30ac..2d62a33618 100644 --- a/src/cache.cpp +++ b/src/cache.cpp @@ -301,6 +301,8 @@ namespace { assert(bmp); + if (s.oob_check && (T == Material::System && Player::IsPatchManiac())) return bmp; + if (s.oob_check) { int w = bmp->GetWidth(); int h = bmp->GetHeight(); diff --git a/src/font.cpp b/src/font.cpp index e51b23862a..8ecddb898f 100644 --- a/src/font.cpp +++ b/src/font.cpp @@ -937,19 +937,35 @@ FontRef Font::exfont = std::make_shared(); Font::GlyphRet ExFont::vRender(char32_t glyph) const { if (EP_UNLIKELY(!bm)) { bm = Bitmap::Create(WIDTH, HEIGHT, true); } auto exfont = Cache::Exfont(); + bm->Clear(); - bool is_lower = (glyph >= 'a' && glyph <= 'z'); - bool is_upper = (glyph >= 'A' && glyph <= 'Z'); + Rect rect(0, 0, 0, 0); - if (!is_lower && !is_upper) { - // Invalid ExFont - return { bm, {WIDTH, 0}, {0, 0}, false }; + if (Player::IsPatchManiac() && (glyph & Utils::EXFONT_XY_FLAG)) { + // Maniacs Patch $[x,y] or $[n] mode + int x = glyph & 0xFF; + int y = (glyph >> 8) & 0xFF; + rect = Rect(x * WIDTH, y * HEIGHT, WIDTH, HEIGHT); } + else { + // Standard $[A-Za-z] or Maniacs $[A] mode + bool is_lower = (glyph >= 'a' && glyph <= 'z'); + bool is_upper = (glyph >= 'A' && glyph <= 'Z'); - glyph = is_lower ? (glyph - 'a' + 26) : (glyph - 'A'); + if (!is_lower && !is_upper) { + // Invalid ExFont character + return { bm, {WIDTH, 0}, {0, 0}, false }; + } + + char32_t adjusted_glyph = is_lower ? (glyph - 'a' + 26) : (glyph - 'A'); + rect = Rect((adjusted_glyph % 13) * WIDTH, (adjusted_glyph / 13) * HEIGHT, WIDTH, HEIGHT); + } + + if (rect.x + rect.width > exfont->GetWidth() || rect.y + rect.height > exfont->GetHeight()) { + // Coordinates are out of bounds for the ExFont sheet + return { bm, {WIDTH, 0}, {0, 0}, false }; + } - Rect const rect((glyph % 13) * WIDTH, (glyph / 13) * HEIGHT, WIDTH, HEIGHT); - bm->Clear(); bm->Blit(0, 0, *exfont, rect, Opacity::Opaque()); // EasyRPG Extension: Support for colored ExFont diff --git a/src/game_message.cpp b/src/game_message.cpp index 2ae087c106..cb1bcae54f 100644 --- a/src/game_message.cpp +++ b/src/game_message.cpp @@ -185,6 +185,83 @@ std::optional Game_Message::CommandCodeInserter(char ch, const char return PendingMessage::DefaultCommandInserter(ch, iter, end, escape_char); } +Game_Message::ParseArrayResult Game_Message::ParseArray(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, {}, false }; + } + auto ret = Utils::UTF8Next(iter, end); + if (ret.ch != escape_char) { + return { begin, {}, false }; + } + iter = ret.next; + if (iter != end) { // Skip the command character (e.g., 'C' in \C) + auto cmd_char_ret = Utils::UTF8Next(iter, end); + iter = cmd_char_ret.next; + } + } + + if (iter == end || *iter != '[') { + return { iter, {}, false }; + } + ++iter; + + // Helper lambda to parse one number, supporting \V[n] + auto parse_one_number = [&](int& out_val) { + bool stop_parsing = false; + while (iter != end && *iter != ']' && *iter != ',') { + if (stop_parsing) { ++iter; continue; } + + if (*iter >= '0' && *iter <= '9') { + out_val = out_val * 10 + (*iter - '0'); + ++iter; + continue; + } + + if (max_recursion > 0) { + auto ret = Utils::UTF8Next(iter, end); + if (ret.ch == escape_char && ret.next != end && (*ret.next == 'V' || *ret.next == 'v')) { + auto var_ret = ParseVariable(ret.next, end, escape_char, false, max_recursion - 1); + int var_val = Main_Data::game_variables->Get(var_ret.value); + int m = 10; + if (out_val != 0) { while (m <= std::abs(var_val)) m *= 10; } + out_val = out_val * m + var_val; + iter = var_ret.next; + continue; + } + } + stop_parsing = true; + } + }; + + ParseArrayResult result; + result.next = iter; + + if (iter != end && *iter != ']') { + do { + int current_value = 0; + parse_one_number(current_value); + result.values.push_back(current_value); + + if (iter != end && *iter == ',') { + result.is_array = true; + ++iter; + } + else { + break; + } + } while (iter != end && *iter != ']'); + } + + if (iter != end && *iter == ']') { + ++iter; + } + + result.next = iter; + return result; +} + Game_Message::ParseParamResult Game_Message::ParseParam( char upper, char lower, diff --git a/src/game_message.h b/src/game_message.h index 59959e826c..da69c0da06 100644 --- a/src/game_message.h +++ b/src/game_message.h @@ -106,6 +106,17 @@ namespace Game_Message { // Which one we'll use by default. static constexpr int default_max_recursion = easyrpg_default_max_recursion; + /** Struct returned by maniacs patch array parsing */ + struct ParseArrayResult { + const char* next; + std::vector values; + // is_array will be true if at least one comma was found, + // indicating multiple values were intended. e.g., for `[123]`, is_array is false. for `[1,2]`, it's true. + bool is_array = false; + }; + + ParseArrayResult ParseArray(const char* iter, const char* end, uint32_t escape_char, bool skip_prefix = false, int max_recursion = 4); + /** Struct returned by parameter parsing methods */ struct ParseParamResult { /** iterator to the next character after parsed content */ diff --git a/src/game_windows.cpp b/src/game_windows.cpp index b95974d07d..46cb472da8 100644 --- a/src/game_windows.cpp +++ b/src/game_windows.cpp @@ -429,22 +429,32 @@ void Game_Windows::Window_User::Refresh(bool& async_wait) { // Special message codes switch (ch) { - case 'c': - case 'C': - { - // Color - auto pres = Game_Message::ParseColor(text_index, end, Player::escape_char, true); - auto value = pres.value; + case 'c': + case 'C': + { + // Color + if (Player::IsPatchManiac()) { + auto pres = Game_Message::ParseArray(text_index, end, Player::escape_char, true); text_index = pres.next; - if (Player::IsPatchManiac()) { - text_color = value; - } - else { - text_color = value > 19 ? 0 : value; + if (!pres.values.empty()) { + if (pres.is_array && pres.values.size() >= 2) { + // Maniacs \C[x,y] -> y * 10 + x + text_color = pres.values[1] * 10 + pres.values[0]; + } + else { + // Maniacs \C[n] + text_color = pres.values[0]; + } } } - break; + else { + auto pres = Game_Message::ParseColor(text_index, end, Player::escape_char, true); + text_index = pres.next; + text_color = pres.value > 19 ? 0 : pres.value; + } + } + break; } continue; } diff --git a/src/utils.cpp b/src/utils.cpp index 8600ddb359..6663c5ebb9 100644 --- a/src/utils.cpp +++ b/src/utils.cpp @@ -18,6 +18,9 @@ // Headers #include "utils.h" #include "compiler.h" +#include "game_message.h" +#include "player.h" + #include #include #include @@ -408,17 +411,60 @@ int Utils::UTF8Length(std::string_view str) { Utils::ExFontRet Utils::ExFontNext(const char* iter, const char* end) { ExFontRet ret; - if (end - iter >= 2 && *iter == '$') { - auto next_ch = *(iter + 1); - // Don't use std::isalpha, because it's indirects based on locale. - bool is_lower = (next_ch >= 'a' && next_ch <= 'z'); - bool is_upper = (next_ch >= 'A' && next_ch <= 'Z'); - if (is_lower || is_upper) { - ret.next = iter + 2; - ret.value = next_ch; - ret.is_valid = true; + if (end - iter < 2 || *iter != '$') { + return ret; // Not an ExFont command. + } + + // --- Maniacs Patch Extended Syntax Handling --- + if (Player::IsPatchManiac() && *(iter + 1) == '[') { + const char* start_bracket = iter + 1; + const char* end_bracket = std::find(start_bracket + 1, end, ']'); + + if (end_bracket != end) { + std::string_view content(start_bracket + 1, end_bracket - (start_bracket + 1)); + if (content.length() == 1 && std::isalpha(static_cast(content[0]))) { + // AZ Mode: $[A] + ret.next = end_bracket + 1; + ret.value = content[0]; + ret.is_valid = true; + return ret; + } + + auto pres = Game_Message::ParseArray(start_bracket, end, Player::escape_char, true); + if (!pres.values.empty()) { + if (pres.is_array && pres.values.size() >= 2) { + // XY Mode: $[x,y] + uint32_t x = pres.values[0]; + uint32_t y = pres.values[1]; + ret.next = pres.next; + ret.value = EXFONT_XY_FLAG | (y << 8) | x; + ret.is_valid = true; + return ret; + } + else if (!pres.is_array && pres.values.size() == 1) { + // Index Mode: $[n] or $[\V[n]] + int icon_index = pres.values[0]; + int x = icon_index % 13; + int y = icon_index / 13; + ret.next = pres.next; + ret.value = EXFONT_XY_FLAG | (static_cast(y) << 8) | static_cast(x); + ret.is_valid = true; + return ret; + } + } } } + + // --- Standard $A-Z Syntax (and Fallback) --- + auto next_ch = *(iter + 1); + bool is_lower = (next_ch >= 'a' && next_ch <= 'z'); + bool is_upper = (next_ch >= 'A' && next_ch <= 'Z'); + if (is_lower || is_upper) { + ret.next = iter + 2; + ret.value = next_ch; + ret.is_valid = true; + } + return ret; } diff --git a/src/utils.h b/src/utils.h index dbd083bb02..15b1759e76 100644 --- a/src/utils.h +++ b/src/utils.h @@ -165,9 +165,11 @@ namespace Utils { */ std::string FromWideString(const std::wstring& str); + constexpr uint32_t EXFONT_XY_FLAG = (1 << 30); + struct ExFontRet { const char* next = nullptr; - char value = '\0'; + uint32_t value = 0; // Changed from char to uint32_t bool is_valid = false; explicit operator bool() const { return is_valid; } diff --git a/src/window_message.cpp b/src/window_message.cpp index b98eb6f8a7..4adf17f189 100644 --- a/src/window_message.cpp +++ b/src/window_message.cpp @@ -574,22 +574,33 @@ void Window_Message::UpdateMessage() { switch (ch) { case 'c': case 'C': - { - // Color - auto pres = Game_Message::ParseColor(text_index, end, Player::escape_char, true); - auto value = pres.value; + { + // Color + if (Player::IsPatchManiac()) { + auto pres = Game_Message::ParseArray(text_index, end, Player::escape_char, true); text_index = pres.next; - DebugLogText("{}: MSG Color \\c[{}]", value); - SetWaitForNonPrintable(0); - if (Player::IsPatchManiac()) { - text_color = value; - } - else { - text_color = value > 19 ? 0 : value; + if (!pres.values.empty()) { + if (pres.is_array && pres.values.size() >= 2) { + // Maniacs \C[x,y] -> y * 10 + x + text_color = pres.values[1] * 10 + pres.values[0]; + } + else { + // Maniacs \C[n] + text_color = pres.values[0]; + } } } - break; + else { + auto pres = Game_Message::ParseColor(text_index, end, Player::escape_char, true); + text_index = pres.next; + text_color = pres.value > 19 ? 0 : pres.value; + } + + DebugLogText("{}: MSG Color \\c[{}]", text_color); + SetWaitForNonPrintable(0); + } + break; case 's': case 'S': { From 033fa1d1ad03d31cf779574b2b8bc83552d7fa44 Mon Sep 17 00:00:00 2001 From: Ghabry Date: Sun, 14 Dec 2025 14:59:13 +0100 Subject: [PATCH 3/4] Improve new Maniac Array parsing code Message parsing: Merge ParseParam and ParseArray function ExFont: Use the X-Y integer packing also for normal ExFont. Remove EXFONT_XY_FLAG. Add/Update unit tests to verify implementation. --- src/cache.cpp | 12 +++-- src/font.cpp | 17 +------ src/game_message.cpp | 101 ++++++++--------------------------------- src/game_message.h | 19 +++----- src/game_windows.cpp | 25 ++++------ src/utils.cpp | 72 ++++++++++++++--------------- src/utils.h | 4 +- src/window_message.cpp | 23 ++++------ tests/parse.cpp | 74 ++++++++++++++++++++++++++++++ tests/utf.cpp | 4 +- 10 files changed, 163 insertions(+), 188 deletions(-) diff --git a/src/cache.cpp b/src/cache.cpp index 2d62a33618..2fe8f32e47 100644 --- a/src/cache.cpp +++ b/src/cache.cpp @@ -301,8 +301,6 @@ namespace { assert(bmp); - if (s.oob_check && (T == Material::System && Player::IsPatchManiac())) return bmp; - if (s.oob_check) { int w = bmp->GetWidth(); int h = bmp->GetHeight(); @@ -318,8 +316,14 @@ namespace { // EasyRPG extensions add support for large charsets; size is spoofed to ignore the error if (!filename.empty() && filename.front() == '$' && T == Material::Charset && Player::HasEasyRpgExtensions()) { - w = 288; - h = 256; + w = min_w; + h = min_h; + } + + // Maniac Patch adds support for more colors; size is spoofed to ignore the error + if (T == Material::System && Player::IsPatchManiac()) { + w = min_w; + h = min_h; } if (w < min_w || max_w < w || h < min_h || max_h < h) { diff --git a/src/font.cpp b/src/font.cpp index 8ecddb898f..ff05a81ffe 100644 --- a/src/font.cpp +++ b/src/font.cpp @@ -941,25 +941,10 @@ Font::GlyphRet ExFont::vRender(char32_t glyph) const { Rect rect(0, 0, 0, 0); - if (Player::IsPatchManiac() && (glyph & Utils::EXFONT_XY_FLAG)) { - // Maniacs Patch $[x,y] or $[n] mode + // Glyph contains two packed coordinates (YX, 8 bits each) int x = glyph & 0xFF; int y = (glyph >> 8) & 0xFF; rect = Rect(x * WIDTH, y * HEIGHT, WIDTH, HEIGHT); - } - else { - // Standard $[A-Za-z] or Maniacs $[A] mode - bool is_lower = (glyph >= 'a' && glyph <= 'z'); - bool is_upper = (glyph >= 'A' && glyph <= 'Z'); - - if (!is_lower && !is_upper) { - // Invalid ExFont character - return { bm, {WIDTH, 0}, {0, 0}, false }; - } - - char32_t adjusted_glyph = is_lower ? (glyph - 'a' + 26) : (glyph - 'A'); - rect = Rect((adjusted_glyph % 13) * WIDTH, (adjusted_glyph / 13) * HEIGHT, WIDTH, HEIGHT); - } if (rect.x + rect.width > exfont->GetWidth() || rect.y + rect.height > exfont->GetHeight()) { // Coordinates are out of bounds for the ExFont sheet diff --git a/src/game_message.cpp b/src/game_message.cpp index cb1bcae54f..0404cbd713 100644 --- a/src/game_message.cpp +++ b/src/game_message.cpp @@ -185,83 +185,6 @@ std::optional Game_Message::CommandCodeInserter(char ch, const char return PendingMessage::DefaultCommandInserter(ch, iter, end, escape_char); } -Game_Message::ParseArrayResult Game_Message::ParseArray(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, {}, false }; - } - auto ret = Utils::UTF8Next(iter, end); - if (ret.ch != escape_char) { - return { begin, {}, false }; - } - iter = ret.next; - if (iter != end) { // Skip the command character (e.g., 'C' in \C) - auto cmd_char_ret = Utils::UTF8Next(iter, end); - iter = cmd_char_ret.next; - } - } - - if (iter == end || *iter != '[') { - return { iter, {}, false }; - } - ++iter; - - // Helper lambda to parse one number, supporting \V[n] - auto parse_one_number = [&](int& out_val) { - bool stop_parsing = false; - while (iter != end && *iter != ']' && *iter != ',') { - if (stop_parsing) { ++iter; continue; } - - if (*iter >= '0' && *iter <= '9') { - out_val = out_val * 10 + (*iter - '0'); - ++iter; - continue; - } - - if (max_recursion > 0) { - auto ret = Utils::UTF8Next(iter, end); - if (ret.ch == escape_char && ret.next != end && (*ret.next == 'V' || *ret.next == 'v')) { - auto var_ret = ParseVariable(ret.next, end, escape_char, false, max_recursion - 1); - int var_val = Main_Data::game_variables->Get(var_ret.value); - int m = 10; - if (out_val != 0) { while (m <= std::abs(var_val)) m *= 10; } - out_val = out_val * m + var_val; - iter = var_ret.next; - continue; - } - } - stop_parsing = true; - } - }; - - ParseArrayResult result; - result.next = iter; - - if (iter != end && *iter != ']') { - do { - int current_value = 0; - parse_one_number(current_value); - result.values.push_back(current_value); - - if (iter != end && *iter == ',') { - result.is_array = true; - ++iter; - } - else { - break; - } - } while (iter != end && *iter != ']'); - } - - if (iter != end && *iter == ']') { - ++iter; - } - - result.next = iter; - return result; -} - Game_Message::ParseParamResult Game_Message::ParseParam( char upper, char lower, @@ -269,7 +192,8 @@ Game_Message::ParseParamResult Game_Message::ParseParam( const char* end, uint32_t escape_char, bool skip_prefix, - int max_recursion) + int max_recursion, + bool parse_array) { if (!skip_prefix) { const auto begin = iter; @@ -294,6 +218,7 @@ Game_Message::ParseParamResult Game_Message::ParseParam( } int value = 0; + std::vector values; ++iter; bool stop_parsing = false; bool got_valid_number = false; @@ -318,6 +243,14 @@ Game_Message::ParseParamResult Game_Message::ParseParam( auto ch = ret.ch; iter = ret.next; + if (parse_array && ch == ',') { + // command contains a list of numbers + values.push_back(value); + value = 0; + got_valid_number = false; + continue; + } + // Recursive variable case. if (ch == escape_char) { if (iter != end && (*iter == 'V' || *iter == 'v')) { @@ -350,15 +283,17 @@ Game_Message::ParseParamResult Game_Message::ParseParam( ++iter; } + values.emplace_back(value); + // Actor 0 references the first party member - if (upper == 'N' && value == 0 && got_valid_number) { + if (upper == 'N' && values.front() == 0 && got_valid_number) { auto* party = Main_Data::game_party.get(); if (party->GetBattlerCount() > 0) { - value = (*party)[0].GetId(); + values.front() = (*party)[0].GetId(); } } - return { iter, value }; + return { iter, values.front(), values }; } Game_Message::ParseParamStringResult Game_Message::ParseStringParam( @@ -435,8 +370,8 @@ Game_Message::ParseParamResult Game_Message::ParseString(const char* iter, const return ParseParam('T', 't', iter, end, escape_char, skip_prefix, max_recursion); } -Game_Message::ParseParamResult Game_Message::ParseColor(const char* iter, const char* end, uint32_t escape_char, bool skip_prefix, int max_recursion) { - return ParseParam('C', 'c', iter, end, escape_char, skip_prefix, max_recursion); +Game_Message::ParseParamResult Game_Message::ParseColor(const char* iter, const char* end, uint32_t escape_char, bool skip_prefix, int max_recursion, bool parse_array) { + return ParseParam('C', 'c', iter, end, escape_char, skip_prefix, max_recursion, parse_array); } Game_Message::ParseParamResult Game_Message::ParseSpeed(const char* iter, const char* end, uint32_t escape_char, bool skip_prefix, int max_recursion) { diff --git a/src/game_message.h b/src/game_message.h index da69c0da06..6d85c96c70 100644 --- a/src/game_message.h +++ b/src/game_message.h @@ -106,23 +106,16 @@ namespace Game_Message { // Which one we'll use by default. static constexpr int default_max_recursion = easyrpg_default_max_recursion; - /** Struct returned by maniacs patch array parsing */ - struct ParseArrayResult { - const char* next; - std::vector values; - // is_array will be true if at least one comma was found, - // indicating multiple values were intended. e.g., for `[123]`, is_array is false. for `[1,2]`, it's true. - bool is_array = false; - }; - - ParseArrayResult ParseArray(const char* iter, const char* end, uint32_t escape_char, bool skip_prefix = false, int max_recursion = 4); - /** Struct returned by parameter parsing methods */ struct ParseParamResult { /** iterator to the next character after parsed content */ const char* next = nullptr; /** value that was parsed */ int value = 0; + /** multiple values in case of array parsing. For compatibility first number is also stored in `value` */ + std::vector values; + + bool is_array() const { return values.size() >= 2; } }; /** Struct returned by parameter parsing methods */ @@ -167,7 +160,7 @@ namespace Game_Message { * * @return \refer ParseParamResult */ - ParseParamResult ParseColor(const char* iter, const char* end, uint32_t escape_char, bool skip_prefix = false, int max_recursion = default_max_recursion); + ParseParamResult ParseColor(const char* iter, const char* end, uint32_t escape_char, bool skip_prefix = false, int max_recursion = default_max_recursion, bool parse_array = false); /** Parse a \s[] speed string * @@ -193,7 +186,7 @@ namespace Game_Message { */ ParseParamResult ParseActor(const char* iter, const char* end, uint32_t escape_char, bool skip_prefix = false, int max_recursion = default_max_recursion); - 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); + 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, bool parse_array = false); // 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); } diff --git a/src/game_windows.cpp b/src/game_windows.cpp index 46cb472da8..d240626b3e 100644 --- a/src/game_windows.cpp +++ b/src/game_windows.cpp @@ -302,7 +302,7 @@ void Game_Windows::Window_User::Refresh(bool& async_wait) { case 'C': { // Color - text_index = Game_Message::ParseColor(text_index, end, Player::escape_char, true).next; + text_index = Game_Message::ParseColor(text_index, end, Player::escape_char, true, Game_Message::default_max_recursion, Player::IsPatchManiac()).next; } break; } @@ -433,24 +433,19 @@ void Game_Windows::Window_User::Refresh(bool& async_wait) { case 'C': { // Color + auto pres = Game_Message::ParseColor(text_index, end, Player::escape_char, true, Game_Message::default_max_recursion, Player::IsPatchManiac()); + text_index = pres.next; + if (Player::IsPatchManiac()) { - auto pres = Game_Message::ParseArray(text_index, end, Player::escape_char, true); - text_index = pres.next; - - if (!pres.values.empty()) { - if (pres.is_array && pres.values.size() >= 2) { - // Maniacs \C[x,y] -> y * 10 + x - text_color = pres.values[1] * 10 + pres.values[0]; - } - else { - // Maniacs \C[n] - text_color = pres.values[0]; - } + if (pres.is_array()) { + // Maniacs \C[x,y] -> y * 10 + x + text_color = pres.values[1] * 10 + pres.values[0]; + } else { + // Maniacs \C[n] (arbitrary amount of colors) + text_color = pres.values[0]; } } else { - auto pres = Game_Message::ParseColor(text_index, end, Player::escape_char, true); - text_index = pres.next; text_color = pres.value > 19 ? 0 : pres.value; } } diff --git a/src/utils.cpp b/src/utils.cpp index 6663c5ebb9..06d588cbea 100644 --- a/src/utils.cpp +++ b/src/utils.cpp @@ -415,53 +415,49 @@ Utils::ExFontRet Utils::ExFontNext(const char* iter, const char* end) { return ret; // Not an ExFont command. } - // --- Maniacs Patch Extended Syntax Handling --- - if (Player::IsPatchManiac() && *(iter + 1) == '[') { - const char* start_bracket = iter + 1; - const char* end_bracket = std::find(start_bracket + 1, end, ']'); - - if (end_bracket != end) { - std::string_view content(start_bracket + 1, end_bracket - (start_bracket + 1)); - if (content.length() == 1 && std::isalpha(static_cast(content[0]))) { - // AZ Mode: $[A] - ret.next = end_bracket + 1; - ret.value = content[0]; - ret.is_valid = true; - return ret; - } + // The (0xFFu << 16) is to avoid detection as a control character by the + // font rendering code - auto pres = Game_Message::ParseArray(start_bracket, end, Player::escape_char, true); - if (!pres.values.empty()) { - if (pres.is_array && pres.values.size() >= 2) { - // XY Mode: $[x,y] - uint32_t x = pres.values[0]; - uint32_t y = pres.values[1]; - ret.next = pres.next; - ret.value = EXFONT_XY_FLAG | (y << 8) | x; - ret.is_valid = true; - return ret; - } - else if (!pres.is_array && pres.values.size() == 1) { - // Index Mode: $[n] or $[\V[n]] - int icon_index = pres.values[0]; - int x = icon_index % 13; - int y = icon_index / 13; - ret.next = pres.next; - ret.value = EXFONT_XY_FLAG | (static_cast(y) << 8) | static_cast(x); - ret.is_valid = true; - return ret; - } - } + // Maniacs Patch Extended Syntax $[x,y] Handling + if (Player::IsPatchManiac() && *(iter + 1) == '[') { + auto pres = Game_Message::ParseParam('$', '$', ++iter, end, Player::escape_char, true, Game_Message::default_max_recursion, true); + if (pres.values.empty()) { + // parse error + return ret; + } + + ret.next = pres.next; + + if (pres.is_array()) { + // XY Mode: $[x,y] + uint32_t x = pres.values[0]; + uint32_t y = pres.values[1]; + ret.value = (0xFFu << 16) | (y << 8) | x; + ret.is_valid = true; + return ret; + } + else if (pres.values.size() == 1) { + // Index Mode: $[n] or $[\V[n]] + int icon_index = pres.values[0]; + int x = icon_index % 13; + int y = icon_index / 13; + ret.value = (0xFFu << 16) | (static_cast(y) << 8) | static_cast(x); + ret.is_valid = true; + return ret; } } - // --- Standard $A-Z Syntax (and Fallback) --- + // Standard $A-Z Syntax auto next_ch = *(iter + 1); bool is_lower = (next_ch >= 'a' && next_ch <= 'z'); bool is_upper = (next_ch >= 'A' && next_ch <= 'Z'); + if (is_lower || is_upper) { ret.next = iter + 2; - ret.value = next_ch; + char32_t adjusted_glyph = is_lower ? (next_ch - 'a' + 26) : (next_ch - 'A'); + int x = adjusted_glyph % 13; + int y = adjusted_glyph / 13; + ret.value = (0xFFu << 16) | (static_cast(y) << 8) | static_cast(x); ret.is_valid = true; } diff --git a/src/utils.h b/src/utils.h index 15b1759e76..29e060c11f 100644 --- a/src/utils.h +++ b/src/utils.h @@ -165,11 +165,9 @@ namespace Utils { */ std::string FromWideString(const std::wstring& str); - constexpr uint32_t EXFONT_XY_FLAG = (1 << 30); - struct ExFontRet { const char* next = nullptr; - uint32_t value = 0; // Changed from char to uint32_t + uint32_t value = 0; bool is_valid = false; explicit operator bool() const { return is_valid; } diff --git a/src/window_message.cpp b/src/window_message.cpp index 4adf17f189..f7bf5eebf9 100644 --- a/src/window_message.cpp +++ b/src/window_message.cpp @@ -576,24 +576,19 @@ void Window_Message::UpdateMessage() { case 'C': { // Color - if (Player::IsPatchManiac()) { - auto pres = Game_Message::ParseArray(text_index, end, Player::escape_char, true); - text_index = pres.next; + auto pres = Game_Message::ParseColor(text_index, end, Player::escape_char, true, Game_Message::default_max_recursion, Player::IsPatchManiac()); + text_index = pres.next; - if (!pres.values.empty()) { - if (pres.is_array && pres.values.size() >= 2) { - // Maniacs \C[x,y] -> y * 10 + x - text_color = pres.values[1] * 10 + pres.values[0]; - } - else { - // Maniacs \C[n] - text_color = pres.values[0]; - } + if (Player::IsPatchManiac()) { + if (pres.is_array()) { + // Maniacs \C[x,y] -> y * 10 + x + text_color = pres.values[1] * 10 + pres.values[0]; + } else { + // Maniacs \C[n] (arbitrary amount of colors) + text_color = pres.values[0]; } } else { - auto pres = Game_Message::ParseColor(text_index, end, Player::escape_char, true); - text_index = pres.next; text_color = pres.value > 19 ? 0 : pres.value; } diff --git a/tests/parse.cpp b/tests/parse.cpp index 2a72dc90e8..af63647bd4 100644 --- a/tests/parse.cpp +++ b/tests/parse.cpp @@ -7,6 +7,7 @@ #include "main_data.h" #include #include "doctest.h" +#include "player.h" TEST_SUITE_BEGIN("Parse"); @@ -338,6 +339,59 @@ TEST_CASE("ColorVarsRecurse") { REQUIRE_EQ(ret.next, (msg.data() + msg.size()) - 1); } +TEST_CASE("Colors ArraySyntax") { + DataInit init; + + std::string msg; + Game_Message::ParseParamResult ret; + + msg = u8"\\c[3]"; + ret = Game_Message::ParseColor(msg.data(), (msg.data() + msg.size()), escape, false, Game_Message::rpg_rt_default_max_recursion, true); + REQUIRE_EQ(ret.value, 3); + REQUIRE(!ret.is_array()); + REQUIRE_EQ(ret.next, (msg.data() + msg.size())); + + msg = u8"\\c[3,10]"; + ret = Game_Message::ParseColor(msg.data(), (msg.data() + msg.size()), escape, false, Game_Message::rpg_rt_default_max_recursion, true); + REQUIRE_EQ(ret.value, 3); + REQUIRE_EQ(ret.values[0], 3); + REQUIRE_EQ(ret.values[1], 10); + REQUIRE(ret.is_array()); + REQUIRE_EQ(ret.next, (msg.data() + msg.size())); + + msg = u8"\\c[32,4]Hello"; + ret = Game_Message::ParseColor(msg.data(), (msg.data() + msg.size()), escape, false, Game_Message::rpg_rt_default_max_recursion, true); + REQUIRE_EQ(ret.value, 32); + REQUIRE_EQ(ret.values[0], 32); + REQUIRE_EQ(ret.values[1], 4); + REQUIRE(ret.is_array()); + REQUIRE_EQ(ret.next, &*msg.data() + 8); + + msg = u8"\\c[3,0004]"; + ret = Game_Message::ParseColor(msg.data(), (msg.data() + msg.size()), escape, false, Game_Message::rpg_rt_default_max_recursion, true); + REQUIRE_EQ(ret.value, 3); + REQUIRE_EQ(ret.values[0], 3); + REQUIRE_EQ(ret.values[1], 4); + REQUIRE(ret.is_array()); + REQUIRE_EQ(ret.next, (msg.data() + msg.size())); + + msg = u8"\\c[3,a]HelloWorld"; + ret = Game_Message::ParseColor(msg.data(), (msg.data() + msg.size()), escape, false, Game_Message::rpg_rt_default_max_recursion, true); + REQUIRE_EQ(ret.value, 3); + REQUIRE_EQ(ret.values[0], 3); + REQUIRE_EQ(ret.values[1], 0); + REQUIRE(ret.is_array()); + REQUIRE_EQ(ret.next, &*msg.data() + 7); + + msg = u8"\\c[3,]"; + ret = Game_Message::ParseColor(msg.data(), (msg.data() + msg.size()), escape, false, Game_Message::rpg_rt_default_max_recursion, true); + REQUIRE_EQ(ret.value, 3); + REQUIRE_EQ(ret.values[0], 3); + REQUIRE_EQ(ret.values[1], 0); + REQUIRE(ret.is_array()); + REQUIRE_EQ(ret.next, (msg.data() + msg.size())); +} + TEST_CASE("Speed") { DataInit init; @@ -475,4 +529,24 @@ TEST_CASE("BadSpeed") { REQUIRE_EQ(ret.next, msg.data()); } +TEST_CASE("ParseManiacExFont") { + DataInit init; + Player::game_config.patch_maniac.Set(true); + + std::string msg; + Utils::ExFontRet ret; + + msg = u8"$[43]"; + ret = Utils::ExFontNext(msg.data(), (msg.data() + msg.size())); + REQUIRE_EQ(ret.value, 0xFF0304); + REQUIRE_EQ(ret.next, msg.data() + msg.size()); + + msg = u8"$[3,40]"; + ret = Utils::ExFontNext(msg.data(), (msg.data() + msg.size())); + REQUIRE_EQ(ret.value, 0xFF2803); + REQUIRE_EQ(ret.next, msg.data() + msg.size()); + + Player::game_config.patch_maniac.Set(false); +} + TEST_SUITE_END(); diff --git a/tests/utf.cpp b/tests/utf.cpp index c6f1d1994e..f5b679bb4e 100644 --- a/tests/utf.cpp +++ b/tests/utf.cpp @@ -130,14 +130,14 @@ TEST_CASE("TextNext") { iter = ret.next; ret = Utils::TextNext(iter, end, escape); - REQUIRE_EQ(ret.ch, 'A'); + REQUIRE_EQ(ret.ch, 0xFF0000); // ExFont 'A' (x=0,y=0) REQUIRE_NE(ret.next, end); REQUIRE(ret.is_exfont); REQUIRE_FALSE(ret.is_escape); iter = ret.next; ret = Utils::TextNext(iter, end, escape); - REQUIRE_EQ(ret.ch, 'B'); + REQUIRE_EQ(ret.ch, 0xFF0001); // ExFont 'B' (x=1,y=0) REQUIRE_NE(ret.next, end); REQUIRE(ret.is_exfont); REQUIRE_FALSE(ret.is_escape); From b812bef329fb587286699da03c280db9145fff54 Mon Sep 17 00:00:00 2001 From: Ghabry Date: Sun, 14 Dec 2025 14:59:52 +0100 Subject: [PATCH 4/4] Color ExFont: Fix color detection The old code sometimes failed to detect colored glyphs --- src/font.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/font.cpp b/src/font.cpp index ff05a81ffe..f1d1d164d2 100644 --- a/src/font.cpp +++ b/src/font.cpp @@ -942,9 +942,9 @@ Font::GlyphRet ExFont::vRender(char32_t glyph) const { Rect rect(0, 0, 0, 0); // Glyph contains two packed coordinates (YX, 8 bits each) - int x = glyph & 0xFF; - int y = (glyph >> 8) & 0xFF; - rect = Rect(x * WIDTH, y * HEIGHT, WIDTH, HEIGHT); + int x = glyph & 0xFF; + int y = (glyph >> 8) & 0xFF; + rect = Rect(x * WIDTH, y * HEIGHT, WIDTH, HEIGHT); if (rect.x + rect.width > exfont->GetWidth() || rect.y + rect.height > exfont->GetHeight()) { // Coordinates are out of bounds for the ExFont sheet @@ -956,8 +956,8 @@ Font::GlyphRet ExFont::vRender(char32_t glyph) const { // EasyRPG Extension: Support for colored ExFont bool has_color = false; const auto* pixels = reinterpret_cast(bm->pixels()); - // For performance reasons only check the red channel of every 4th pixel (16 = 4 * 4 RGBA pixel) for color - for (int i = 0; i < bm->pitch() * bm->height(); i += 16) { + // For performance reasons only check the red channel of every pixel for color + for (int i = 0; i < bm->pitch() * bm->height(); i += 4) { auto pixel = pixels[i]; if (pixel != 0 && pixel != 255) { has_color = true;