diff --git a/src/openrct2/localisation/Formatting.cpp b/src/openrct2/localisation/Formatting.cpp index 03264639eb..57d2e0073d 100644 --- a/src/openrct2/localisation/Formatting.cpp +++ b/src/openrct2/localisation/Formatting.cpp @@ -19,7 +19,7 @@ namespace OpenRCT2 { - static void FormatMonthYear(std::stringstream& ss, int32_t month, int32_t year); + static void FormatMonthYear(FormatBuffer& ss, int32_t month, int32_t year); static std::optional ParseNumericToken(std::string_view s) { @@ -278,7 +278,7 @@ namespace OpenRCT2 return sz != nullptr ? sz : std::string_view(); } - void FormatRealName(std::stringstream& ss, rct_string_id id) + void FormatRealName(FormatBuffer& ss, rct_string_id id) { if (IsRealNameStringId(id)) { @@ -301,7 +301,7 @@ namespace OpenRCT2 } } - template void FormatNumber(std::stringstream& ss, T value) + template void FormatNumber(FormatBuffer& ss, T value) { char buffer[32]; size_t i = 0; @@ -379,7 +379,7 @@ namespace OpenRCT2 } } - template void FormatCurrency(std::stringstream& ss, T rawValue) + template void FormatCurrency(FormatBuffer& ss, T rawValue) { auto currencyDesc = &CurrencyDescriptors[EnumValue(gConfigGeneral.currency_format)]; auto value = static_cast(rawValue) * currencyDesc->rate; @@ -434,7 +434,7 @@ namespace OpenRCT2 } } - template static void FormatMinutesSeconds(std::stringstream& ss, T value) + template static void FormatMinutesSeconds(FormatBuffer& ss, T value) { static constexpr const rct_string_id Formats[][2] = { { STR_DURATION_SEC, STR_DURATION_SECS }, @@ -456,7 +456,7 @@ namespace OpenRCT2 } } - template static void FormatHoursMinutes(std::stringstream& ss, T value) + template static void FormatHoursMinutes(FormatBuffer& ss, T value) { static constexpr const rct_string_id Formats[][2] = { { STR_REALTIME_MIN, STR_REALTIME_MINS }, @@ -478,7 +478,7 @@ namespace OpenRCT2 } } - template void FormatArgument(std::stringstream& ss, FormatToken token, T arg) + template void FormatArgument(FormatBuffer& ss, FormatToken token, T arg) { switch (token) { @@ -593,31 +593,17 @@ namespace OpenRCT2 } break; case FormatToken::String: - if constexpr (std::is_same()) - { - if (arg != nullptr) - { - ss << arg; - } - } - else if constexpr (std::is_same()) - { - ss << arg.c_str(); - } - else if constexpr (std::is_same()) - { - ss << arg.c_str(); - } + ss << arg; break; case FormatToken::Sprite: if constexpr (std::is_integral()) { auto idx = static_cast(arg); - ss << "{INLINE_SPRITE}"; - ss << "{" << ((idx >> 0) & 0xFF) << "}"; - ss << "{" << ((idx >> 8) & 0xFF) << "}"; - ss << "{" << ((idx >> 16) & 0xFF) << "}"; - ss << "{" << ((idx >> 24) & 0xFF) << "}"; + char inlineBuf[64]; + size_t len = snprintf( + inlineBuf, sizeof(inlineBuf), "{INLINE_SPRITE}{%u}{%u}{%u}{%u}", ((idx >> 0) & 0xFF), + ((idx >> 8) & 0xFF), ((idx >> 16) & 0xFF), ((idx >> 24) & 0xFF)); + ss.append(inlineBuf, len); } break; default: @@ -625,12 +611,12 @@ namespace OpenRCT2 } } - template void FormatArgument(std::stringstream&, FormatToken, uint16_t); - template void FormatArgument(std::stringstream&, FormatToken, int16_t); - template void FormatArgument(std::stringstream&, FormatToken, int32_t); - template void FormatArgument(std::stringstream&, FormatToken, int64_t); - template void FormatArgument(std::stringstream&, FormatToken, uint64_t); - template void FormatArgument(std::stringstream&, FormatToken, const char*); + template void FormatArgument(FormatBuffer&, FormatToken, uint16_t); + template void FormatArgument(FormatBuffer&, FormatToken, int16_t); + template void FormatArgument(FormatBuffer&, FormatToken, int32_t); + template void FormatArgument(FormatBuffer&, FormatToken, int64_t); + template void FormatArgument(FormatBuffer&, FormatToken, uint64_t); + template void FormatArgument(FormatBuffer&, FormatToken, const char*); bool IsRealNameStringId(rct_string_id id) { @@ -643,27 +629,24 @@ namespace OpenRCT2 return FmtString(fmtc); } - std::stringstream& GetThreadFormatStream() + FormatBuffer& GetThreadFormatStream() { - thread_local std::stringstream ss; - // Reset the buffer (reported as most efficient way) - std::stringstream().swap(ss); + thread_local FormatBuffer ss; + ss.clear(); return ss; } - size_t CopyStringStreamToBuffer(char* buffer, size_t bufferLen, std::stringstream& ss) + size_t CopyStringStreamToBuffer(char* buffer, size_t bufferLen, FormatBuffer& ss) { - auto stringLen = ss.tellp(); - auto copyLen = std::min(bufferLen - 1, stringLen); + auto copyLen = std::min(bufferLen - 1, ss.size()); - ss.seekg(0, std::ios::beg); - ss.read(buffer, copyLen); + std::copy(ss.data(), ss.data() + copyLen, buffer); buffer[copyLen] = '\0'; - return stringLen; + return ss.size(); } - static void FormatArgumentAny(std::stringstream& ss, FormatToken token, const FormatArg_t& value) + static void FormatArgumentAny(FormatBuffer& ss, FormatToken token, const FormatArg_t& value) { if (std::holds_alternative(value)) { @@ -687,8 +670,7 @@ namespace OpenRCT2 } } - static void FormatStringAny( - std::stringstream& ss, const FmtString& fmt, const std::vector& args, size_t& argIndex) + static void FormatStringAny(FormatBuffer& ss, const FmtString& fmt, const std::vector& args, size_t& argIndex) { for (const auto& token : fmt) { @@ -735,7 +717,7 @@ namespace OpenRCT2 auto& ss = GetThreadFormatStream(); size_t argIndex = 0; FormatStringAny(ss, fmt, args, argIndex); - return ss.str(); + return ss.data(); } size_t FormatStringAny(char* buffer, size_t bufferLen, const FmtString& fmt, const std::vector& args) @@ -815,7 +797,7 @@ namespace OpenRCT2 return FormatStringAny(buffer, bufferLen, fmt, anyArgs); } - static void FormatMonthYear(std::stringstream& ss, int32_t month, int32_t year) + static void FormatMonthYear(FormatBuffer& ss, int32_t month, int32_t year) { thread_local std::vector tempArgs; tempArgs.clear(); diff --git a/src/openrct2/localisation/Formatting.h b/src/openrct2/localisation/Formatting.h index 9b039abd92..cf056c8160 100644 --- a/src/openrct2/localisation/Formatting.h +++ b/src/openrct2/localisation/Formatting.h @@ -13,6 +13,7 @@ #include "FormatCodes.h" #include "Language.h" +#include #include #include #include @@ -23,6 +24,127 @@ namespace OpenRCT2 { + template> class FormatBufferBase + { + T _storage[StackSize]; + T* _buffer; + size_t _size; + // NOTE: Capacity is on purpose uint32_t to have a fixed position for the flag on each architecture. + uint32_t _capacity; + TTraits _traits; + + static constexpr uint32_t FlagLocalStorage = (1u << 31); + + public: + explicit FormatBufferBase() + : _storage{} + , _buffer(_storage) + , _size{} + , _capacity(FlagLocalStorage | static_cast(StackSize)) + { + } + + ~FormatBufferBase() + { + if (_capacity & FlagLocalStorage) + return; + delete[] _buffer; + } + + size_t size() const + { + return _size; + } + + size_t capacity() const + { + return _capacity & ~FlagLocalStorage; + } + + void clear() + { + _size = 0; + _buffer[0] = T{}; + } + + const T* data() const + { + return _buffer; + } + + T* data() + { + return _buffer; + } + + template auto& operator<<(T const (&v)[N]) + { + append(v, N); + return *this; + } + + auto& operator<<(const T v) + { + append(&v, 1); + return *this; + } + + auto& operator<<(const T* v) + { + if (!v) + return *this; + + append(v, _traits.length(v)); + return *this; + } + + auto& operator<<(const std::basic_string_view v) + { + append(v.data(), v.size()); + return *this; + } + + auto& operator<<(const std::basic_string& v) + { + append(v.data(), v.size()); + return *this; + } + + void append(const T* buf, size_t len) + { + ensure_capacity(len); + + std::copy(buf, buf + len, _buffer + _size); + + _size += len; + _buffer[_size] = T{}; + } + + private: + void ensure_capacity(size_t additionalSize) + { + const size_t curSize = size(); + const size_t curCapacity = capacity(); + const bool isLocalStorage = _capacity & FlagLocalStorage; + + if (curSize + additionalSize < curCapacity) + return; + + const size_t newCapacity = (curCapacity + additionalSize + 1) << 1; + + T* newBuf = new T[newCapacity]; + std::copy(_buffer, _buffer + curSize, newBuf); + + if (!isLocalStorage) + delete[] _buffer; + + _capacity = static_cast(newCapacity); + _buffer = newBuf; + } + }; + + using FormatBuffer = FormatBufferBase; + using FormatArg_t = std::variant; class FmtString @@ -76,15 +198,15 @@ namespace OpenRCT2 std::string WithoutFormatTokens() const; }; - template void FormatArgument(std::stringstream& ss, FormatToken token, T arg); + template void FormatArgument(FormatBuffer& ss, FormatToken token, T arg); bool IsRealNameStringId(rct_string_id id); - void FormatRealName(std::stringstream& ss, rct_string_id id); + void FormatRealName(FormatBuffer& ss, rct_string_id id); FmtString GetFmtStringById(rct_string_id id); - std::stringstream& GetThreadFormatStream(); - size_t CopyStringStreamToBuffer(char* buffer, size_t bufferLen, std::stringstream& ss); + FormatBuffer& GetThreadFormatStream(); + size_t CopyStringStreamToBuffer(char* buffer, size_t bufferLen, FormatBuffer& ss); - inline void FormatString(std::stringstream& ss, std::stack& stack) + inline void FormatString(FormatBuffer& ss, std::stack& stack) { while (!stack.empty()) { @@ -103,7 +225,7 @@ namespace OpenRCT2 } template - static void FormatString(std::stringstream& ss, std::stack& stack, TArg0 arg0, TArgs&&... argN) + static void FormatString(FormatBuffer& ss, std::stack& stack, TArg0 arg0, TArgs&&... argN) { while (!stack.empty()) { @@ -144,7 +266,7 @@ namespace OpenRCT2 } } - template static void FormatString(std::stringstream& ss, const FmtString& fmt, TArgs&&... argN) + template static void FormatString(FormatBuffer& ss, const FmtString& fmt, TArgs&&... argN) { std::stack stack; stack.push(fmt.begin()); @@ -155,7 +277,7 @@ namespace OpenRCT2 { auto& ss = GetThreadFormatStream(); FormatString(ss, fmt, argN...); - return ss.str(); + return ss.data(); } template @@ -166,7 +288,7 @@ namespace OpenRCT2 return CopyStringStreamToBuffer(buffer, bufferLen, ss); } - template static void FormatStringId(std::stringstream& ss, rct_string_id id, TArgs&&... argN) + template static void FormatStringId(FormatBuffer& ss, rct_string_id id, TArgs&&... argN) { auto fmt = GetFmtStringById(id); FormatString(ss, fmt, argN...); diff --git a/test/tests/FormattingTests.cpp b/test/tests/FormattingTests.cpp index b34ec5e15b..7f7790b0b5 100644 --- a/test/tests/FormattingTests.cpp +++ b/test/tests/FormattingTests.cpp @@ -341,260 +341,260 @@ TEST_F(FormattingTests, using_legacy_buffer_args) TEST_F(FormattingTests, format_number_basic) { - std::stringstream ss; + FormatBuffer ss; // test basic integral conversion FormatArgument(ss, FormatToken::UInt16, 123); - ASSERT_STREQ("123", ss.str().c_str()); + ASSERT_STREQ("123", ss.data()); } TEST_F(FormattingTests, format_number_basic_int32) { - std::stringstream ss; + FormatBuffer ss; // test that case fallthrough works FormatArgument(ss, FormatToken::Int32, 123); - ASSERT_STREQ("123", ss.str().c_str()); + ASSERT_STREQ("123", ss.data()); } TEST_F(FormattingTests, format_number_negative) { - std::stringstream ss; + FormatBuffer ss; // test negative conversion FormatArgument(ss, FormatToken::Int32, -123); - ASSERT_STREQ("-123", ss.str().c_str()); + ASSERT_STREQ("-123", ss.data()); } TEST_F(FormattingTests, format_number_comma16_basic) { - std::stringstream ss; + FormatBuffer ss; // test separator formatter // test base case separator formatter FormatArgument(ss, FormatToken::Comma16, 123); - ASSERT_STREQ("123", ss.str().c_str()); + ASSERT_STREQ("123", ss.data()); } TEST_F(FormattingTests, format_number_comma16_negative) { - std::stringstream ss; + FormatBuffer ss; // test separator formatter // test base case separator formatter FormatArgument(ss, FormatToken::Comma16, -123); - ASSERT_STREQ("-123", ss.str().c_str()); + ASSERT_STREQ("-123", ss.data()); } TEST_F(FormattingTests, format_number_comma16_large) { - std::stringstream ss; + FormatBuffer ss; // test larger value for separator formatter FormatArgument(ss, FormatToken::Comma16, 123456789); - ASSERT_STREQ("123,456,789", ss.str().c_str()); + ASSERT_STREQ("123,456,789", ss.data()); } TEST_F(FormattingTests, format_number_comma16_large_negative) { - std::stringstream ss; + FormatBuffer ss; // test larger value for separator formatter with negative FormatArgument(ss, FormatToken::Comma16, -123456789); - ASSERT_STREQ("-123,456,789", ss.str().c_str()); + ASSERT_STREQ("-123,456,789", ss.data()); } TEST_F(FormattingTests, format_number_comma16_uneven) { - std::stringstream ss; + FormatBuffer ss; // test non-multiple of 3 FormatArgument(ss, FormatToken::Comma16, 12345678); - ASSERT_STREQ("12,345,678", ss.str().c_str()); + ASSERT_STREQ("12,345,678", ss.data()); } TEST_F(FormattingTests, format_number_comma16_uneven_negative) { - std::stringstream ss; + FormatBuffer ss; // test non-multiple of 3 with negative FormatArgument(ss, FormatToken::Comma16, -12345678); - ASSERT_STREQ("-12,345,678", ss.str().c_str()); + ASSERT_STREQ("-12,345,678", ss.data()); } TEST_F(FormattingTests, format_number_comma16_zero) { - std::stringstream ss; + FormatBuffer ss; // test zero FormatArgument(ss, FormatToken::Comma16, 0); - ASSERT_STREQ("0", ss.str().c_str()); + ASSERT_STREQ("0", ss.data()); } TEST_F(FormattingTests, format_number_comma1dp16_zero) { - std::stringstream ss; + FormatBuffer ss; // zero case FormatArgument(ss, FormatToken::Comma1dp16, 0); - ASSERT_STREQ("0.0", ss.str().c_str()); + ASSERT_STREQ("0.0", ss.data()); } TEST_F(FormattingTests, format_number_comma1dp16_leading_zero) { - std::stringstream ss; + FormatBuffer ss; // test leading zero FormatArgument(ss, FormatToken::Comma1dp16, 5); - ASSERT_STREQ("0.5", ss.str().c_str()); + ASSERT_STREQ("0.5", ss.data()); } TEST_F(FormattingTests, format_number_comma1dp16_leading_zero_negative) { - std::stringstream ss; + FormatBuffer ss; // test leading zero with negative value FormatArgument(ss, FormatToken::Comma1dp16, -5); - ASSERT_STREQ("-0.5", ss.str().c_str()); + ASSERT_STREQ("-0.5", ss.data()); } TEST_F(FormattingTests, format_number_comma1dp16_small_value) { - std::stringstream ss; + FormatBuffer ss; // test small value FormatArgument(ss, FormatToken::Comma1dp16, 75); - ASSERT_STREQ("7.5", ss.str().c_str()); + ASSERT_STREQ("7.5", ss.data()); } TEST_F(FormattingTests, format_number_comma1dp16_small_value_negative) { - std::stringstream ss; + FormatBuffer ss; // test small value with negative FormatArgument(ss, FormatToken::Comma1dp16, -75); - ASSERT_STREQ("-7.5", ss.str().c_str()); + ASSERT_STREQ("-7.5", ss.data()); } TEST_F(FormattingTests, format_number_comma1dp16_trailing_zeros) { - std::stringstream ss; + FormatBuffer ss; // test value with trailing zero, no commas FormatArgument(ss, FormatToken::Comma1dp16, 1000); - ASSERT_STREQ("100.0", ss.str().c_str()); + ASSERT_STREQ("100.0", ss.data()); } TEST_F(FormattingTests, format_number_comma1dp16_trailing_zeros_negative) { - std::stringstream ss; + FormatBuffer ss; // test value with trailing zero, no commas FormatArgument(ss, FormatToken::Comma1dp16, -1000); - ASSERT_STREQ("-100.0", ss.str().c_str()); + ASSERT_STREQ("-100.0", ss.data()); } TEST_F(FormattingTests, format_number_comma1dp16_large_trailing_zeros) { - std::stringstream ss; + FormatBuffer ss; // test value with commas and trailing zeros FormatArgument(ss, FormatToken::Comma1dp16, 10000000); - ASSERT_STREQ("1,000,000.0", ss.str().c_str()); + ASSERT_STREQ("1,000,000.0", ss.data()); } TEST_F(FormattingTests, format_number_comma1dp16_large_trailing_zeros_negative) { - std::stringstream ss; + FormatBuffer ss; // test value with commas and trailing zeros FormatArgument(ss, FormatToken::Comma1dp16, -10000000); - ASSERT_STREQ("-1,000,000.0", ss.str().c_str()); + ASSERT_STREQ("-1,000,000.0", ss.data()); } TEST_F(FormattingTests, format_number_comma1dp16_large_value) { - std::stringstream ss; + FormatBuffer ss; // test large value FormatArgument(ss, FormatToken::Comma1dp16, 123456789); - ASSERT_STREQ("12,345,678.9", ss.str().c_str()); + ASSERT_STREQ("12,345,678.9", ss.data()); } TEST_F(FormattingTests, format_number_comma1dp16_large_value_negative) { - std::stringstream ss; + FormatBuffer ss; // test large value FormatArgument(ss, FormatToken::Comma1dp16, -123456789); - ASSERT_STREQ("-12,345,678.9", ss.str().c_str()); + ASSERT_STREQ("-12,345,678.9", ss.data()); } TEST_F(FormattingTests, format_number_comma2dp32_zero) { - std::stringstream ss; + FormatBuffer ss; // zero case FormatArgument(ss, FormatToken::Comma2dp32, 0); - ASSERT_STREQ("0.00", ss.str().c_str()); + ASSERT_STREQ("0.00", ss.data()); } TEST_F(FormattingTests, format_number_comma2dp32_less_sig_figs) { - std::stringstream ss; + FormatBuffer ss; // test leading zero FormatArgument(ss, FormatToken::Comma2dp32, 5); - ASSERT_STREQ("0.05", ss.str().c_str()); + ASSERT_STREQ("0.05", ss.data()); } TEST_F(FormattingTests, format_number_comma2dp32_less_sig_figs_negative) { - std::stringstream ss; + FormatBuffer ss; // test leading zero FormatArgument(ss, FormatToken::Comma2dp32, -5); - ASSERT_STREQ("-0.05", ss.str().c_str()); + ASSERT_STREQ("-0.05", ss.data()); } TEST_F(FormattingTests, format_number_comma2dp32_leading_zero) { - std::stringstream ss; + FormatBuffer ss; // test small value FormatArgument(ss, FormatToken::Comma2dp32, 75); - ASSERT_STREQ("0.75", ss.str().c_str()); + ASSERT_STREQ("0.75", ss.data()); } TEST_F(FormattingTests, format_number_comma2dp32_leading_zero_negative) { - std::stringstream ss; + FormatBuffer ss; // test small value FormatArgument(ss, FormatToken::Comma2dp32, -75); - ASSERT_STREQ("-0.75", ss.str().c_str()); + ASSERT_STREQ("-0.75", ss.data()); } TEST_F(FormattingTests, format_number_comma2dp32_trailing_zeros) { - std::stringstream ss; + FormatBuffer ss; // test value with trailing zero, no commas FormatArgument(ss, FormatToken::Comma2dp32, 1000); - ASSERT_STREQ("10.00", ss.str().c_str()); + ASSERT_STREQ("10.00", ss.data()); } TEST_F(FormattingTests, format_number_comma2dp32_trailing_zeros_negative) { - std::stringstream ss; + FormatBuffer ss; // test value with trailing zero, no commas FormatArgument(ss, FormatToken::Comma2dp32, -1000); - ASSERT_STREQ("-10.00", ss.str().c_str()); + ASSERT_STREQ("-10.00", ss.data()); } TEST_F(FormattingTests, format_number_comma2dp32_large_trailing_zeros) { - std::stringstream ss; + FormatBuffer ss; // test value with commas and trailing zeros FormatArgument(ss, FormatToken::Comma2dp32, 10000000); - ASSERT_STREQ("100,000.00", ss.str().c_str()); + ASSERT_STREQ("100,000.00", ss.data()); } TEST_F(FormattingTests, format_number_comma2dp32_large_trailing_zeros_negative) { - std::stringstream ss; + FormatBuffer ss; // test value with commas and trailing zeros FormatArgument(ss, FormatToken::Comma2dp32, -10000000); - ASSERT_STREQ("-100,000.00", ss.str().c_str()); + ASSERT_STREQ("-100,000.00", ss.data()); } TEST_F(FormattingTests, format_number_comma2dp32_large_value) { - std::stringstream ss; + FormatBuffer ss; // test large value FormatArgument(ss, FormatToken::Comma2dp32, 123456789); - ASSERT_STREQ("1,234,567.89", ss.str().c_str()); + ASSERT_STREQ("1,234,567.89", ss.data()); } TEST_F(FormattingTests, format_number_comma2dp32_large_value_negative) { - std::stringstream ss; + FormatBuffer ss; // test large value FormatArgument(ss, FormatToken::Comma2dp32, -123456789); - ASSERT_STREQ("-1,234,567.89", ss.str().c_str()); + ASSERT_STREQ("-1,234,567.89", ss.data()); // extra note: // for some reason the FormatArgument function contains constexpr @@ -604,3 +604,14 @@ TEST_F(FormattingTests, format_number_comma2dp32_large_value_negative) // the necessary symbol to link with. // FormatArgument(ss, FormatToken::Comma1dp16, 12.372); } + +TEST_F(FormattingTests, buffer_storage_swap) +{ + FormatBufferBase ss; + ss << "Hello World"; + ASSERT_STREQ(ss.data(), "Hello World"); + ss << ", Exceeding local storage"; + ASSERT_STREQ(ss.data(), "Hello World, Exceeding local storage"); + ss << ", extended"; + ASSERT_STREQ(ss.data(), "Hello World, Exceeding local storage, extended"); +}