diff --git a/src/cache.cpp b/src/cache.cpp index 6852bb30ac..2fe8f32e47 100644 --- a/src/cache.cpp +++ b/src/cache.cpp @@ -316,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 e51b23862a..f1d1d164d2 100644 --- a/src/font.cpp +++ b/src/font.cpp @@ -937,26 +937,27 @@ 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(); + + Rect rect(0, 0, 0, 0); - bool is_lower = (glyph >= 'a' && glyph <= 'z'); - bool is_upper = (glyph >= 'A' && glyph <= 'Z'); + // 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); - if (!is_lower && !is_upper) { - // Invalid ExFont + 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 }; } - glyph = is_lower ? (glyph - 'a' + 26) : (glyph - 'A'); - - 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 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; diff --git a/src/game_message.cpp b/src/game_message.cpp index 2ae087c106..0404cbd713 100644 --- a/src/game_message.cpp +++ b/src/game_message.cpp @@ -192,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; @@ -217,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; @@ -241,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')) { @@ -273,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( @@ -358,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 59959e826c..6d85c96c70 100644 --- a/src/game_message.h +++ b/src/game_message.h @@ -112,6 +112,10 @@ namespace Game_Message { 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 */ @@ -156,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 * @@ -182,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 8cd753e008..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; } @@ -429,16 +429,27 @@ 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; - text_index = pres.next; - text_color = value > 19 ? 0 : value; + case 'c': + 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()) { + 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 { + text_color = pres.value > 19 ? 0 : pres.value; } - break; + } + break; } continue; } diff --git a/src/utils.cpp b/src/utils.cpp index 8600ddb359..06d588cbea 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,56 @@ 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; + if (end - iter < 2 || *iter != '$') { + return ret; // Not an ExFont command. + } + + // The (0xFFu << 16) is to avoid detection as a control character by the + // font rendering code + + // 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 + 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; + 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; + } + return ret; } diff --git a/src/utils.h b/src/utils.h index dbd083bb02..29e060c11f 100644 --- a/src/utils.h +++ b/src/utils.h @@ -167,7 +167,7 @@ namespace Utils { struct ExFontRet { const char* next = nullptr; - char value = '\0'; + 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 4d797a29de..f7bf5eebf9 100644 --- a/src/window_message.cpp +++ b/src/window_message.cpp @@ -574,16 +574,28 @@ 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; - text_index = pres.next; - DebugLogText("{}: MSG Color \\c[{}]", value); - SetWaitForNonPrintable(0); - text_color = value > 19 ? 0 : value; + { + // 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()) { + 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]; + } } - break; + else { + text_color = pres.value > 19 ? 0 : pres.value; + } + + DebugLogText("{}: MSG Color \\c[{}]", text_color); + SetWaitForNonPrintable(0); + } + break; case 's': case 'S': { 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);