diff --git a/Source/CMakeLists.txt b/Source/CMakeLists.txt index 0b4f25939..85d5746b9 100644 --- a/Source/CMakeLists.txt +++ b/Source/CMakeLists.txt @@ -797,8 +797,8 @@ if(USE_SDL1) utils/sdl2_to_1_2_backports.cpp ) target_link_dependencies(libdevilutionx_sdl2_to_1_2_backports PRIVATE + libdevilutionx_strings libdevilutionx_utils_console - fmt::fmt ) target_link_libraries(DevilutionX::SDL INTERFACE libdevilutionx_sdl2_to_1_2_backports diff --git a/Source/capture.cpp b/Source/capture.cpp index 820e21de2..30993e15d 100644 --- a/Source/capture.cpp +++ b/Source/capture.cpp @@ -11,7 +11,6 @@ #include #include -#include #define DEVILUTIONX_SCREENSHOT_FORMAT_PCX 0 #define DEVILUTIONX_SCREENSHOT_FORMAT_PNG 1 @@ -46,8 +45,9 @@ SDL_RWops *CaptureFile(std::string *dstPath) const std::time_t tt = std::time(nullptr); const std::tm *tm = std::localtime(&tt); const std::string filename = tm != nullptr - ? fmt::format("Screenshot from {:04}-{:02}-{:02} {:02}-{:02}-{:02}", - tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec) + ? StrCat("Screenshot from ", + LeftPad(tm->tm_year + 1900, 4, '0'), "-", LeftPad(tm->tm_mon + 1, 2, '0'), "-", LeftPad(tm->tm_mday, 2, '0'), "-", + LeftPad(tm->tm_hour, 2, '0'), "-", LeftPad(tm->tm_min, 2, '0'), "-", LeftPad(tm->tm_sec, 2, '0')) : "Screenshot"; *dstPath = StrCat(paths::PrefPath(), filename, ext); int i = 0; diff --git a/Source/engine/assets.cpp b/Source/engine/assets.cpp index 412756846..dd031a109 100644 --- a/Source/engine/assets.cpp +++ b/Source/engine/assets.cpp @@ -367,7 +367,7 @@ std::vector GetMPQSearchPaths() std::string message; for (std::size_t i = 0; i < paths.size(); ++i) { - message.append(fmt::format("\n{:6d}. '{}'", i + 1, paths[i])); + message.append(StrCat("\n", LeftPad(i + 1, 6, ' '), ". '", paths[i], "'")); } LogVerbose("MPQ search paths:{}", message); } diff --git a/Source/engine/demomode.cpp b/Source/engine/demomode.cpp index 821311e94..1d1d60367 100644 --- a/Source/engine/demomode.cpp +++ b/Source/engine/demomode.cpp @@ -5,8 +5,6 @@ #include #include -#include - #ifdef USE_SDL1 #include "utils/sdl2_to_1_2_backports.h" #endif @@ -180,7 +178,7 @@ void ReadSettings(FILE *in, uint8_t version) // NOLINT(readability-identifier-le DemoSettings = {}; } - std::string message = fmt::format("⚙️\n{}={}x{}", _("Resolution"), DemoGraphicsWidth, DemoGraphicsHeight); + std::string message = StrCat("⚙️\n", _("Resolution"), "=", DemoGraphicsWidth, "x", DemoGraphicsHeight); for (const auto &[key, value] : std::initializer_list> { { _("Run in Town"), DemoSettings.runInTown }, { _("Theo Quest"), DemoSettings.theoQuest }, @@ -199,7 +197,7 @@ void ReadSettings(FILE *in, uint8_t version) // NOLINT(readability-identifier-le { _("Show Item Labels"), DemoSettings.showItemLabels }, { _("Auto Refill Belt"), DemoSettings.autoRefillBelt }, { _("Disable Crippling Shrines"), DemoSettings.disableCripplingShrines } }) { - fmt::format_to(std::back_inserter(message), "\n{}={:d}", key, value); + StrAppend(message, "\n", key, "=", value ? "1" : "0"); } for (const auto &[key, value] : std::initializer_list> { { _("Heal Potion Pickup"), DemoSettings.numHealPotionPickup }, @@ -208,7 +206,7 @@ void ReadSettings(FILE *in, uint8_t version) // NOLINT(readability-identifier-le { _("Full Mana Potion Pickup"), DemoSettings.numFullManaPotionPickup }, { _("Rejuvenation Potion Pickup"), DemoSettings.numRejuPotionPickup }, { _("Full Rejuvenation Potion Pickup"), DemoSettings.numFullRejuPotionPickup } }) { - fmt::format_to(std::back_inserter(message), "\n{}={}", key, value); + StrAppend(message, "\n", key, "=", static_cast(value)); } Log("{}", message); } @@ -776,8 +774,8 @@ void RecordMessage(const SDL_Event &event, uint16_t modState) || event.wheel.x > std::numeric_limits::max() || event.wheel.y < std::numeric_limits::min() || event.wheel.y > std::numeric_limits::max()) { - app_fatal(fmt::format("Mouse wheel event x/y out of int16_t range. x={} y={}", - event.wheel.x, event.wheel.y)); + app_fatal(StrCat("Mouse wheel event x/y out of int16_t range. x=", + event.wheel.x, " y=", event.wheel.y)); } WriteLE16(DemoRecording, event.wheel.x); WriteLE16(DemoRecording, event.wheel.y); diff --git a/Source/engine/palette.cpp b/Source/engine/palette.cpp index d187ee47a..3189c2b14 100644 --- a/Source/engine/palette.cpp +++ b/Source/engine/palette.cpp @@ -11,8 +11,6 @@ #include #include -#include - #include "engine/backbuffer_state.hpp" #include "engine/demomode.h" #include "engine/dx.h" @@ -24,6 +22,7 @@ #include "utils/display.h" #include "utils/palette_blending.hpp" #include "utils/sdl_compat.h" +#include "utils/str_cat.hpp" namespace devilution { @@ -214,9 +213,12 @@ void LoadRndLvlPal(dungeon_type l) if (!*GetOptions().Graphics.alternateNestArt) { rv++; } - *fmt::format_to(szFileName, R"(nlevels\l{0}data\l{0}base{1}.pal)", 6, rv) = '\0'; + *BufCopy(szFileName, R"(nlevels\l6data\l6base)", rv, ".pal") = '\0'; } else { - *fmt::format_to(szFileName, R"(levels\l{0}data\l{0}_{1}.pal)", static_cast(l), rv) = '\0'; + char nbuf[3]; + const char *end = BufCopy(nbuf, static_cast(l)); + const std::string_view n = std::string_view(nbuf, end - nbuf); + *BufCopy(szFileName, "levels\\l", n, "data\\l", n, "_", rv, ".pal") = '\0'; } LoadPaletteAndInitBlending(szFileName); } diff --git a/Source/items.cpp b/Source/items.cpp index c2f205a1e..20083385d 100644 --- a/Source/items.cpp +++ b/Source/items.cpp @@ -3573,7 +3573,7 @@ void CornerstoneSave() PackItem(id, CornerStone.item, (CornerStone.item.dwBuff & CF_HELLFIRE) != 0); const auto *buffer = reinterpret_cast(&id); for (size_t i = 0; i < sizeof(ItemPack); i++) { - fmt::format_to(&GetOptions().Hellfire.szItem[i * 2], "{:02X}", buffer[i]); + BufCopy(&GetOptions().Hellfire.szItem[i * 2], AsHexPad2(buffer[i], /*uppercase=*/true)); } GetOptions().Hellfire.szItem[sizeof(GetOptions().Hellfire.szItem) - 1] = '\0'; } else { diff --git a/Source/loadsave.cpp b/Source/loadsave.cpp index 6f9126c91..7ea7fa254 100644 --- a/Source/loadsave.cpp +++ b/Source/loadsave.cpp @@ -15,7 +15,6 @@ #include #include #include -#include #include "automap.h" #include "codec.h" @@ -1049,7 +1048,7 @@ void GetLevelNames(std::string_view prefix, char *out) suf = 'l'; num = currlevel; } - *fmt::format_to(out, "{}{}{:02d}", prefix, suf, num) = '\0'; + *BufCopy(out, prefix, std::string_view(&suf, 1), LeftPad(num, 2, '0')) = '\0'; } void GetTempLevelNames(char *szTemp) diff --git a/Source/pfile.cpp b/Source/pfile.cpp index 67e6d483e..c070abe09 100644 --- a/Source/pfile.cpp +++ b/Source/pfile.cpp @@ -11,7 +11,6 @@ #include #include -#include #include "codec.h" #include "engine/load_file.hpp" @@ -92,7 +91,7 @@ bool GetSaveNames(uint8_t index, std::string_view prefix, char *out) return false; } - *fmt::format_to(out, "{}{}{:02d}", prefix, suf, index) = '\0'; + *BufCopy(out, prefix, std::string_view(&suf, 1), LeftPad(index, 2, '0')) = '\0'; return true; } diff --git a/Source/player.cpp b/Source/player.cpp index 1e439e867..15a38e561 100644 --- a/Source/player.cpp +++ b/Source/player.cpp @@ -1516,6 +1516,11 @@ uint16_t GetPlayerSpriteWidth(HeroClass cls, player_graphic graphic, PlayerWeapo app_fatal("Invalid player_graphic"); } +void GetPlayerGraphicsPath(std::string_view path, std::string_view prefix, std::string_view type, char out[256]) +{ + *BufCopy(out, "plrgfx\\", path, "\\", prefix, "\\", prefix, type) = '\0'; +} + } // namespace void Player::CalcScrolls() @@ -2054,11 +2059,7 @@ ClxSprite GetPlayerPortraitSprite(Player &player) const PlayerSpriteData &spriteData = GetPlayerSpriteDataForClass(cls); const char *path = spriteData.classPath.c_str(); - const char *szCel; - if (!inDungeon) - szCel = "st"; - else - szCel = "as"; + std::string_view szCel = inDungeon ? "as" : "st"; player_graphic graphic = player_graphic::Stand; if (player._pHitPoints <= 0) { @@ -2068,9 +2069,9 @@ ClxSprite GetPlayerPortraitSprite(Player &player) } } - char prefix[3] = { spriteData.classChar, ArmourChar[player._pgfxnum >> 4], WepChar[static_cast(animWeaponId)] }; + const char prefixBuf[3] = { spriteData.classChar, ArmourChar[player._pgfxnum >> 4], WepChar[static_cast(animWeaponId)] }; char pszName[256]; - *fmt::format_to(pszName, R"(plrgfx\{0}\{1}\{1}{2})", path, std::string_view(prefix, 3), szCel) = 0; + GetPlayerGraphicsPath(path, std::string_view(prefixBuf, 3), szCel, pszName); const std::string spritePath { pszName }; // Check to see if the sprite has updated. @@ -2110,7 +2111,7 @@ void LoadPlrGFX(Player &player, player_graphic graphic) const PlayerSpriteData &spriteData = GetPlayerSpriteDataForClass(cls); const char *path = spriteData.classPath.c_str(); - const char *szCel; + std::string_view szCel; switch (graphic) { case player_graphic::Stand: szCel = "as"; @@ -2157,9 +2158,9 @@ void LoadPlrGFX(Player &player, player_graphic graphic) app_fatal("PLR:2"); } - char prefix[3] = { spriteData.classChar, ArmourChar[player._pgfxnum >> 4], WepChar[static_cast(animWeaponId)] }; + const char prefixBuf[3] = { spriteData.classChar, ArmourChar[player._pgfxnum >> 4], WepChar[static_cast(animWeaponId)] }; char pszName[256]; - *fmt::format_to(pszName, R"(plrgfx\{0}\{1}\{1}{2})", path, std::string_view(prefix, 3), szCel) = 0; + GetPlayerGraphicsPath(path, std::string_view(prefixBuf, 3), szCel, pszName); const uint16_t animationWidth = GetPlayerSpriteWidth(cls, graphic, animWeaponId); animationData.sprites = LoadCl2Sheet(pszName, animationWidth); std::optional> graphicTRN = GetPlayerGraphicTRN(pszName); diff --git a/Source/qol/chatlog.cpp b/Source/qol/chatlog.cpp index 526d19a4b..a2d1dbb03 100644 --- a/Source/qol/chatlog.cpp +++ b/Source/qol/chatlog.cpp @@ -24,6 +24,7 @@ #include "minitext.h" #include "stores.h" #include "utils/language.h" +#include "utils/str_cat.hpp" namespace devilution { @@ -119,8 +120,9 @@ void AddMessageToChatLog(std::string_view message, Player *player, UiFlags flags MessageCounter++; const time_t timeResult = time(nullptr); const std::tm *localtimeResult = localtime(&timeResult); - std::string timestamp = localtimeResult != nullptr ? fmt::format("[#{:d}] {:02}:{:02}:{:02}", MessageCounter, localtimeResult->tm_hour, localtimeResult->tm_min, localtimeResult->tm_sec) - : fmt::format("[#{:d}] ", MessageCounter); + std::string timestamp = localtimeResult != nullptr + ? StrCat("[#", MessageCounter, "] ", LeftPad(localtimeResult->tm_hour, 2, '0'), ":", LeftPad(localtimeResult->tm_min, 2, '0'), ":", LeftPad(localtimeResult->tm_sec, 2, '0')) + : StrCat("[#", MessageCounter, "] "); const size_t oldSize = ChatLogLines.size(); if (player == nullptr) { ChatLogLines.emplace_back(MultiColoredText { "{0} {1}", { { timestamp, UiFlags::ColorRed }, { std::string(message), flags } } }); @@ -172,7 +174,8 @@ void DrawChatLog(const Surface &out) const time_t timeResult = time(nullptr); const std::tm *localtimeResult = localtime(&timeResult); if (localtimeResult != nullptr) { - const std::string timestamp = fmt::format("{:02}:{:02}:{:02}", localtimeResult->tm_hour, localtimeResult->tm_min, localtimeResult->tm_sec); + const std::string timestamp = StrCat( + LeftPad(localtimeResult->tm_hour, 2, '0'), ":", LeftPad(localtimeResult->tm_min, 2, '0'), ":", LeftPad(localtimeResult->tm_sec, 2, '0')); DrawString(out, timestamp, { { sx, sy + PaddingTop + blankLineHeight }, { ContentTextWidth, lineHeight } }, { .flags = UiFlags::ColorWhitegold }); } diff --git a/Source/utils/sdl2_to_1_2_backports.cpp b/Source/utils/sdl2_to_1_2_backports.cpp index 6befb7911..14414a695 100644 --- a/Source/utils/sdl2_to_1_2_backports.cpp +++ b/Source/utils/sdl2_to_1_2_backports.cpp @@ -15,9 +15,8 @@ #include #endif -#include - #include "utils/console.h" +#include "utils/str_cat.hpp" #define DEFAULT_PRIORITY SDL_LOG_PRIORITY_CRITICAL #define DEFAULT_ASSERT_PRIORITY SDL_LOG_PRIORITY_WARN @@ -919,9 +918,9 @@ extern "C" char *SDL_GetPrefPath(const char *org, const char *app) } if (*org) { - *fmt::format_to_n(retval, len - 1, "{}{}{}/{}", envr, append, org, app).out = '\0'; + *devilution::BufCopy(retval, envr, append, org, "/", app) = '\0'; } else { - *fmt::format_to_n(retval, len - 1, "{}{}{}", envr, append, app).out = '\0'; + *devilution::BufCopy(retval, envr, append, app) = '\0'; } for (ptr = retval + 1; *ptr; ptr++) { diff --git a/Source/utils/str_cat.cpp b/Source/utils/str_cat.cpp index 157c2e8d3..90097137b 100644 --- a/Source/utils/str_cat.cpp +++ b/Source/utils/str_cat.cpp @@ -9,6 +9,7 @@ namespace devilution { namespace { [[nodiscard]] char HexDigit(uint8_t v) { return "0123456789abcdef"[v]; } +[[nodiscard]] char HexDigitUpper(uint8_t v) { return "0123456789ABCDEF"[v]; } } // namespace @@ -26,8 +27,13 @@ char *BufCopy(char *out, unsigned long long value) } char *BufCopy(char *out, AsHexU8Pad2 value) { - *out++ = HexDigit(value.value >> 4); - *out++ = HexDigit(value.value & 0xf); + if (value.uppercase) { + *out++ = HexDigitUpper(value.value >> 4); + *out++ = HexDigitUpper(value.value & 0xf); + } else { + *out++ = HexDigit(value.value >> 4); + *out++ = HexDigit(value.value & 0xf); + } return out; } char *BufCopy(char *out, AsHexU16Pad2 value) @@ -36,7 +42,7 @@ char *BufCopy(char *out, AsHexU16Pad2 value) if (value.value > 0xfff) { out = BufCopy(out, AsHexU8Pad2 { static_cast(value.value >> 8) }); } else { - *out++ = HexDigit(value.value >> 8); + *out++ = value.uppercase ? HexDigitUpper(value.value >> 8) : HexDigit(value.value >> 8); } } return BufCopy(out, AsHexU8Pad2 { static_cast(value.value & 0xff) }); diff --git a/Source/utils/str_cat.hpp b/Source/utils/str_cat.hpp index ac939b687..5c5504788 100644 --- a/Source/utils/str_cat.hpp +++ b/Source/utils/str_cat.hpp @@ -10,21 +10,42 @@ namespace devilution { struct AsHexU8Pad2 { uint8_t value; + bool uppercase = false; }; struct AsHexU16Pad2 { uint16_t value; + bool uppercase = false; +}; + +/** @brief Maximum number of digits for a value of type T. + * If T is signed, also accounts for a potential '-'. + */ +template +constexpr size_t MaxNumDigits = (8 * sizeof(T) * 28 / 93) + 1 + (std::is_signed_v ? 1 : 0); + +template +struct LeftPadT { + T value; + unsigned length; + char padChar; }; /** * @brief Formats the value as a lowercase zero-padded hexadecimal with at least 2 hex digits (0-padded on the left). */ -constexpr AsHexU8Pad2 AsHexPad2(uint8_t value) { return { value }; } +constexpr AsHexU8Pad2 AsHexPad2(uint8_t value, bool uppercase = false) { return { value, uppercase }; } /** * @brief Formats the value as a lowercase zero-padded hexadecimal with at least 2 hex digits (0-padded on the left). */ -constexpr AsHexU16Pad2 AsHexPad2(uint16_t value) { return { value }; } +constexpr AsHexU16Pad2 AsHexPad2(uint16_t value, bool uppercase = false) { return { value, uppercase }; } + +/** + * @brief Left-pads the value to the specified length with the specified character. + */ +template +constexpr LeftPadT LeftPad(T value, unsigned length, char padChar) { return { value, length, padChar }; } /** * @brief Writes the integer to the given buffer. @@ -65,6 +86,19 @@ inline char *BufCopy(char *out, unsigned short value) char *BufCopy(char *out, AsHexU8Pad2 value); char *BufCopy(char *out, AsHexU16Pad2 value); +template +char *BufCopy(char *out, LeftPadT value) +{ + char buf[MaxNumDigits]; + const char *end = BufCopy(buf, value.value); + const std::string_view str = std::string_view(buf, end - buf); + for (size_t i = str.size(); i < value.length; ++i) { + *out++ = value.padChar; + } + std::memcpy(out, str.data(), str.size()); + return out + str.size(); +} + /** * @brief Appends the integer to the given string. */ @@ -102,6 +136,14 @@ inline void StrAppend(std::string &out, unsigned short value) void StrAppend(std::string &out, AsHexU8Pad2 value); void StrAppend(std::string &out, AsHexU16Pad2 value); +template +void StrAppend(std::string &out, LeftPadT value) +{ + char buf[MaxNumDigits]; + const auto len = static_cast(BufCopy(buf, value) - buf); + out.append(buf, len); +} + /** * @brief Copies the given std::string_view to the given buffer. */ diff --git a/test/str_cat_test.cpp b/test/str_cat_test.cpp index 3aa6280ca..c3331b09c 100644 --- a/test/str_cat_test.cpp +++ b/test/str_cat_test.cpp @@ -1,3 +1,5 @@ +#include + #include #include "utils/str_cat.hpp" @@ -10,6 +12,11 @@ TEST(StrCatTest, StrCatBasicTest) EXPECT_EQ(StrCat("a", "b", "c", 5), "abc5"); } +TEST(StrCatTest, LeftPadTest) +{ + EXPECT_EQ(StrCat(LeftPad(static_cast(5), 2, '0')), "05"); +} + TEST(StrCatTest, BufCopyBasicTest) { char buf[5];