/***************************************************************************** * Copyright (c) 2014-2024 OpenRCT2 developers * * For a complete list of all authors, please refer to contributors.md * Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2 * * OpenRCT2 is licensed under the GNU General Public License version 3. *****************************************************************************/ #include "Localisation.h" #include "../Diagnostic.h" #include "../Game.h" #include "../config/Config.h" #include "../core/Guard.hpp" #include "../core/String.hpp" #include "../ride/Ride.h" #include "../util/Util.h" #include "Currency.h" #include "FormatCodes.h" #include "Formatting.h" #include "Localisation.Date.h" #include "StringIds.h" #include using namespace OpenRCT2; thread_local char gCommonStringFormatBuffer[CommonTextBufferSize]; // clang-format off const StringId ObjectiveNames[] = { STR_OBJECTIVE_NONE, STR_OBJECTIVE_GUESTS_BY, STR_OBJECTIVE_PARK_VALUE_BY, STR_OBJECTIVE_HAVE_FUN, STR_OBJECTIVE_BUILD_THE_BEST, STR_OBJECTIVE_10_ROLLERCOASTERS, STR_OBJECTIVE_GUESTS_AND_RATING, STR_OBJECTIVE_MONTHLY_RIDE_INCOME, STR_OBJECTIVE_10_ROLLERCOASTERS_LENGTH, STR_OBJECTIVE_FINISH_5_ROLLERCOASTERS, STR_OBJECTIVE_REPLAY_LOAN_AND_PARK_VALUE, STR_OBJECTIVE_MONTHLY_FOOD_INCOME, }; const StringId ResearchFundingLevelNames[] = { STR_RESEARCH_FUNDING_NONE, STR_RESEARCH_FUNDING_MINIMUM, STR_RESEARCH_FUNDING_NORMAL, STR_RESEARCH_FUNDING_MAXIMUM, }; const StringId MarketingCampaignNames[ADVERTISING_CAMPAIGN_COUNT][3] = { { STR_MARKETING_VOUCHERS_FOR_FREE_ENTRY_TO_THE_PARK, STR_VOUCHERS_FOR_FREE_ENTRY_TO, STR_MARKETING_FINISHED_FREE_ENTRY }, // ADVERTISING_CAMPAIGN_PARK_ENTRY_FREE, { STR_MARKETING_VOUCHERS_FOR_FREE_RIDES_ON_A_PARTICULAR_RIDE, STR_VOUCHERS_FOR_FREE_RIDE_ON, STR_MARKETING_FINISHED_FREE_RIDES }, // ADVERTISING_CAMPAIGN_RIDE_FREE, { STR_MARKETING_VOUCHERS_FOR_HALF_PRICE_ENTRY_TO_THE_PARK, STR_VOUCHERS_FOR_HALF_PRICE_ENTRY_TO, STR_MARKETING_FINISHED_HALF_PRICE_ENTRY }, // ADVERTISING_CAMPAIGN_PARK_ENTRY_HALF_PRICE, { STR_MARKETING_VOUCHERS_FOR_FREE_FOOD_OR_DRINK, STR_VOUCHERS_FOR_FREE, STR_MARKETING_FINISHED_FREE_RIDE }, // ADVERTISING_CAMPAIGN_FOOD_OR_DRINK_FREE, { STR_MARKETING_ADVERTISING_CAMPAIGN_FOR_THE_PARK, STR_ADVERTISING_CAMPAIGN_FOR_1, STR_MARKETING_FINISHED_PARK_ADS }, // ADVERTISING_CAMPAIGN_PARK, { STR_MARKETING_ADVERTISING_CAMPAIGN_FOR_A_PARTICULAR_RIDE, STR_ADVERTISING_CAMPAIGN_FOR_2, STR_MARKETING_FINISHED_RIDE_ADS }, // ADVERTISING_CAMPAIGN_RIDE, }; const StringId RideInspectionIntervalNames[] = { STR_EVERY_10_MINUTES, STR_EVERY_20_MINUTES, STR_EVERY_30_MINUTES, STR_EVERY_45_MINUTES, STR_EVERY_HOUR, STR_EVERY_2_HOURS, STR_NEVER, }; const StringId PeepThoughts[] = { STR_PEEP_THOUGHT_TYPE_CANT_AFFORD_0, STR_PEEP_THOUGHT_TYPE_SPENT_MONEY, STR_PEEP_THOUGHT_TYPE_SICK, STR_PEEP_THOUGHT_TYPE_VERY_SICK, STR_PEEP_THOUGHT_TYPE_MORE_THRILLING, STR_PEEP_THOUGHT_TYPE_INTENSE, STR_PEEP_THOUGHT_TYPE_HAVENT_FINISHED, STR_PEEP_THOUGHT_TYPE_SICKENING, STR_PEEP_THOUGHT_TYPE_BAD_VALUE, STR_PEEP_THOUGHT_TYPE_GO_HOME, STR_PEEP_THOUGHT_TYPE_GOOD_VALUE, STR_PEEP_THOUGHT_TYPE_ALREADY_GOT, STR_PEEP_THOUGHT_TYPE_CANT_AFFORD, STR_PEEP_THOUGHT_TYPE_NOT_HUNGRY, STR_PEEP_THOUGHT_TYPE_NOT_THIRSTY, STR_PEEP_THOUGHT_TYPE_DROWNING, STR_PEEP_THOUGHT_TYPE_LOST, STR_PEEP_THOUGHT_TYPE_WAS_GREAT, STR_PEEP_THOUGHT_TYPE_QUEUING_AGES, STR_PEEP_THOUGHT_TYPE_TIRED, STR_PEEP_THOUGHT_TYPE_HUNGRY, STR_PEEP_THOUGHT_TYPE_THIRSTY, STR_PEEP_THOUGHT_TYPE_TOILET, STR_PEEP_THOUGHT_TYPE_CANT_FIND, STR_PEEP_THOUGHT_TYPE_NOT_PAYING, STR_PEEP_THOUGHT_TYPE_NOT_WHILE_RAINING, STR_PEEP_THOUGHT_TYPE_BAD_LITTER, STR_PEEP_THOUGHT_TYPE_CANT_FIND_EXIT, STR_PEEP_THOUGHT_TYPE_GET_OFF, STR_PEEP_THOUGHT_TYPE_GET_OUT, STR_PEEP_THOUGHT_TYPE_NOT_SAFE, STR_PEEP_THOUGHT_TYPE_PATH_DISGUSTING, STR_PEEP_THOUGHT_TYPE_CROWDED, STR_PEEP_THOUGHT_TYPE_VANDALISM, STR_PEEP_THOUGHT_TYPE_SCENERY, STR_PEEP_THOUGHT_TYPE_VERY_CLEAN, STR_PEEP_THOUGHT_TYPE_FOUNTAINS, STR_PEEP_THOUGHT_TYPE_MUSIC, STR_PEEP_THOUGHT_TYPE_BALLOON, STR_PEEP_THOUGHT_TYPE_TOY, STR_PEEP_THOUGHT_TYPE_MAP, STR_PEEP_THOUGHT_TYPE_PHOTO, STR_PEEP_THOUGHT_TYPE_UMBRELLA, STR_PEEP_THOUGHT_TYPE_DRINK, STR_PEEP_THOUGHT_TYPE_BURGER, STR_PEEP_THOUGHT_TYPE_CHIPS, STR_PEEP_THOUGHT_TYPE_ICE_CREAM, STR_PEEP_THOUGHT_TYPE_CANDYFLOSS, 0, 0, 0, STR_PEEP_THOUGHT_TYPE_PIZZA, 0, STR_PEEP_THOUGHT_TYPE_POPCORN, STR_PEEP_THOUGHT_TYPE_HOT_DOG, STR_PEEP_THOUGHT_TYPE_TENTACLE, STR_PEEP_THOUGHT_TYPE_HAT, STR_PEEP_THOUGHT_TYPE_TOFFEE_APPLE, STR_PEEP_THOUGHT_TYPE_TSHIRT, STR_PEEP_THOUGHT_TYPE_DOUGHNUT, STR_PEEP_THOUGHT_TYPE_COFFEE, 0, STR_PEEP_THOUGHT_TYPE_CHICKEN, STR_PEEP_THOUGHT_TYPE_LEMONADE, 0, 0, 0, STR_PEEP_THOUGHT_TYPE_WOW, 0, 0, STR_PEEP_THOUGHT_TYPE_WOW2, STR_PEEP_THOUGHT_TYPE_WATCHED, STR_PEEP_THOUGHT_TYPE_BALLOON_MUCH, STR_PEEP_THOUGHT_TYPE_TOY_MUCH, STR_PEEP_THOUGHT_TYPE_MAP_MUCH, STR_PEEP_THOUGHT_TYPE_PHOTO_MUCH, STR_PEEP_THOUGHT_TYPE_UMBRELLA_MUCH, STR_PEEP_THOUGHT_TYPE_DRINK_MUCH, STR_PEEP_THOUGHT_TYPE_BURGER_MUCH, STR_PEEP_THOUGHT_TYPE_CHIPS_MUCH, STR_PEEP_THOUGHT_TYPE_ICE_CREAM_MUCH, STR_PEEP_THOUGHT_TYPE_CANDYFLOSS_MUCH, 0, 0, 0, STR_PEEP_THOUGHT_TYPE_PIZZA_MUCH, 0, STR_PEEP_THOUGHT_TYPE_POPCORN_MUCH, STR_PEEP_THOUGHT_TYPE_HOT_DOG_MUCH, STR_PEEP_THOUGHT_TYPE_TENTACLE_MUCH, STR_PEEP_THOUGHT_TYPE_HAT_MUCH, STR_PEEP_THOUGHT_TYPE_TOFFEE_APPLE_MUCH, STR_PEEP_THOUGHT_TYPE_TSHIRT_MUCH, STR_PEEP_THOUGHT_TYPE_DOUGHNUT_MUCH, STR_PEEP_THOUGHT_TYPE_COFFEE_MUCH, 0, STR_PEEP_THOUGHT_TYPE_CHICKEN_MUCH, STR_PEEP_THOUGHT_TYPE_LEMONADE_MUCH, 0, 0, 0, 0, 0, 0, STR_PEEP_THOUGHT_TYPE_PHOTO2, STR_PEEP_THOUGHT_TYPE_PHOTO3, STR_PEEP_THOUGHT_TYPE_PHOTO4, STR_PEEP_THOUGHT_TYPE_PRETZEL, STR_PEEP_THOUGHT_TYPE_HOT_CHOCOLATE, STR_PEEP_THOUGHT_TYPE_ICED_TEA, STR_PEEP_THOUGHT_TYPE_FUNNEL_CAKE, STR_PEEP_THOUGHT_TYPE_SUNGLASSES, STR_PEEP_THOUGHT_TYPE_BEEF_NOODLES, STR_PEEP_THOUGHT_TYPE_FRIED_RICE_NOODLES, STR_PEEP_THOUGHT_TYPE_WONTON_SOUP, STR_PEEP_THOUGHT_TYPE_MEATBALL_SOUP, STR_PEEP_THOUGHT_TYPE_FRUIT_JUICE, STR_PEEP_THOUGHT_TYPE_SOYBEAN_MILK, STR_PEEP_THOUGHT_TYPE_SU_JONGKWA, STR_PEEP_THOUGHT_TYPE_SUB_SANDWICH, STR_PEEP_THOUGHT_TYPE_COOKIE, 0, 0, 0, STR_PEEP_THOUGHT_TYPE_ROAST_SAUSAGE, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, STR_PEEP_THOUGHT_TYPE_PHOTO2_MUCH, STR_PEEP_THOUGHT_TYPE_PHOTO3_MUCH, STR_PEEP_THOUGHT_TYPE_PHOTO4_MUCH, STR_PEEP_THOUGHT_TYPE_PRETZEL_MUCH, STR_PEEP_THOUGHT_TYPE_HOT_CHOCOLATE_MUCH, STR_PEEP_THOUGHT_TYPE_ICED_TEA_MUCH, STR_PEEP_THOUGHT_TYPE_FUNNEL_CAKE_MUCH, STR_PEEP_THOUGHT_TYPE_SUNGLASSES_MUCH, STR_PEEP_THOUGHT_TYPE_BEEF_NOODLES_MUCH, STR_PEEP_THOUGHT_TYPE_FRIED_RICE_NOODLES_MUCH, STR_PEEP_THOUGHT_TYPE_WONTON_SOUP_MUCH, STR_PEEP_THOUGHT_TYPE_MEATBALL_SOUP_MUCH, STR_PEEP_THOUGHT_TYPE_FRUIT_JUICE_MUCH, STR_PEEP_THOUGHT_TYPE_SOYBEAN_MILK_MUCH, STR_PEEP_THOUGHT_TYPE_SU_JONGKWA_MUCH, STR_PEEP_THOUGHT_TYPE_SUB_SANDWICH_MUCH, STR_PEEP_THOUGHT_TYPE_COOKIE_MUCH, 0, 0, 0, STR_PEEP_THOUGHT_TYPE_ROAST_SAUSAGE_MUCH, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, STR_PEEP_THOUGHT_TYPE_HELP, STR_PEEP_THOUGHT_TYPE_RUNNING_OUT, STR_PEEP_THOUGHT_TYPE_NEW_RIDE, STR_PEEP_THOUGHT_TYPE_NICE_RIDE_DEPRECATED, STR_PEEP_THOUGHT_TYPE_EXCITED_DEPRECATED, STR_PEEP_THOUGHT_TYPE_HERE_WE_ARE, }; // clang-format on std::string FormatStringIDLegacy(StringId format, const void* args) { std::string buffer(256, 0); size_t len{}; for (;;) { OpenRCT2::FormatStringLegacy(buffer.data(), buffer.size(), format, args); len = buffer.find('\0'); if (len == std::string::npos) { len = buffer.size(); } if (len >= buffer.size() - 1) { // Null terminator to close to end of buffer, grow buffer and try again buffer.resize(buffer.size() * 2); } else { buffer.resize(len); break; } } return buffer; } /** * Writes a formatted string to a buffer and converts it to upper case. * rct2: 0x006C2538 * dest (edi) * format (ax) * args (ecx) */ void FormatStringToUpper(utf8* dest, size_t size, StringId format, const void* args) { if (size == 0) { return; } OpenRCT2::FormatStringLegacy(dest, size, format, args); std::string upperString = String::ToUpper(dest); if (upperString.size() + 1 >= size) { upperString.resize(size - 1); dest[size - 1] = '\0'; LOG_WARNING("Truncating formatted string \"%s\" to %d bytes.", dest, size); } upperString.copy(dest, upperString.size()); dest[upperString.size()] = '\0'; } money64 StringToMoney(const char* string_to_monetise) { const char* decimal_char = LanguageGetString(STR_LOCALE_DECIMAL_POINT); const CurrencyDescriptor* currencyDesc = &CurrencyDescriptors[EnumValue(Config::Get().general.CurrencyFormat)]; char processedString[128] = {}; Guard::Assert(strlen(string_to_monetise) < sizeof(processedString)); uint32_t numNumbers = 0; bool hasMinus = false; bool hasDecSep = false; const char* src_ptr = string_to_monetise; char* dst_ptr = processedString; // Process the string, keeping only numbers decimal, and minus sign(s). while (*src_ptr != '\0') { if (*src_ptr >= '0' && *src_ptr <= '9') { numNumbers++; } else if (*src_ptr == decimal_char[0]) { if (hasDecSep) return kMoney64Undefined; hasDecSep = true; // Replace localised decimal separator with an English one. *dst_ptr++ = '.'; src_ptr++; continue; } else if (*src_ptr == '-') { if (hasMinus) return kMoney64Undefined; hasMinus = true; } else { // Skip invalid characters. src_ptr++; continue; } // Copy numeric values. *dst_ptr++ = *src_ptr; src_ptr++; } // Terminate destination string. *dst_ptr = '\0'; if (numNumbers == 0) return kMoney64Undefined; if (hasMinus && processedString[0] != '-') { // If there is a minus sign, it has to be at position 0 in order to be valid. return kMoney64Undefined; } // Due to the nature of strstr and strtok, decimals at the very beginning will be ignored, causing // ".1" to be interpreted as "1". To prevent this, prefix with "0" if decimal is at the beginning. if (processedString[0] == decimal_char[0]) { for (size_t i = strlen(processedString); i >= 1; i--) processedString[i] = processedString[i - 1]; processedString[0] = '0'; } auto number = std::stod(processedString, nullptr); number /= (currencyDesc->rate / 10.0); return ToMoney64FromGBP(number); } /** * * @param amount The amount in tens of pounds, e.g. 123 = £ 12.30 * @param buffer_to_put_value_to Output parameter. * @param buffer_len Length of the buffer. * @param forceDecimals Show decimals, even if the amount does not have them. Will be ignored if the current exchange * rate is too big to have decimals. */ void MoneyToString(money64 amount, char* buffer_to_put_value_to, size_t buffer_len, bool forceDecimals) { if (amount == kMoney64Undefined) { snprintf(buffer_to_put_value_to, buffer_len, "0"); return; } const CurrencyDescriptor& currencyDesc = CurrencyDescriptors[EnumValue(Config::Get().general.CurrencyFormat)]; const char* sign = amount >= 0 ? "" : "-"; const uint64_t a = std::abs(amount) * currencyDesc.rate; const unsigned long long whole = a / 100; const unsigned long long decimal = a % 100; bool amountIsInteger = (whole > 0) && decimal == 0; // If whole and decimal exist if ((whole > 0 && decimal > 0) || (amountIsInteger && forceDecimals && currencyDesc.rate < 100)) { const char* decimalChar = LanguageGetString(STR_LOCALE_DECIMAL_POINT); auto precedingZero = (decimal < 10) ? "0" : ""; snprintf(buffer_to_put_value_to, buffer_len, "%s%llu%s%s%llu", sign, whole, decimalChar, precedingZero, decimal); } // If whole exists, but not decimal else if (amountIsInteger) { snprintf(buffer_to_put_value_to, buffer_len, "%s%llu", sign, whole); } // If decimal exists, but not whole else if (whole == 0 && decimal > 0) { const char* decimalChar = LanguageGetString(STR_LOCALE_DECIMAL_POINT); snprintf(buffer_to_put_value_to, buffer_len, "%s0%s%llu", sign, decimalChar, decimal); } else { snprintf(buffer_to_put_value_to, buffer_len, "0"); } }