mirror of
https://github.com/OpenRCT2/OpenRCT2
synced 2026-01-20 13:33:02 +01:00
Fix #9999-#10003: translations have truncated strings
This issue, along with several related language-specific trunctions, was traced back to the fact that ScenarioIndexEntry uses a fixed-length array of utf8 characters to store the name, internal name, and scenario details. In some cases, this does not provide enough characters to contain the full description and so the safe copy methods truncate them to fit in the available buffer. Since the use of fixed-size arrays is a holdover from earlier C code, this commit addresses the issue by changing ScenarioIndexEntry to use proper utf8 strings and string views, which do not require truncation.
This commit is contained in:
@@ -247,6 +247,7 @@ Appreciation for contributors who have provided substantial work, but are no lon
|
||||
* Robert Yan (lewyche)
|
||||
* Tom Matalenas (tmatale)
|
||||
* Brendan Heinonen (staticinvocation)
|
||||
* (QuestionableDeer)
|
||||
|
||||
## Toolchain
|
||||
* (Balletie) - macOS
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
0.4.20 (in development)
|
||||
------------------------------------------------------------------------
|
||||
- Improved: [#23677] Building new ride track now inherits the colour scheme from the previous piece.
|
||||
- Fix: [#9999, #10000, #10001, #10002, #10003] Truncated scenario strings when using Catalan, Czech, Japanese, Polish or Russian.
|
||||
- Fix: [#21768] Dirty blocks debug overlay is rendered incorrectly on high DPI screens.
|
||||
- Fix: [#22617] Sloped Wooden and Side-Friction supports draw out of order when built directly above diagonal track pieces.
|
||||
- Fix: [#23522] Diagonal sloped Steeplechase supports have glitched sprites at the base.
|
||||
|
||||
@@ -239,7 +239,7 @@ namespace OpenRCT2::Ui::Windows
|
||||
+ ScreenCoordsXY{ widgets[WIDX_SCENARIOLIST].right + 4, widgets[WIDX_TABCONTENT].top + 5 };
|
||||
auto ft = Formatter();
|
||||
ft.Add<StringId>(STR_STRING);
|
||||
ft.Add<const char*>(scenario->Name);
|
||||
ft.Add<const char*>(scenario->Name.c_str());
|
||||
DrawTextEllipsised(
|
||||
dpi, screenPos + ScreenCoordsXY{ 85, 0 }, 170, STR_WINDOW_COLOUR_2_STRINGID, ft, { TextAlignment::CENTRE });
|
||||
screenPos.y += 15;
|
||||
@@ -247,7 +247,7 @@ namespace OpenRCT2::Ui::Windows
|
||||
// Scenario details
|
||||
ft = Formatter();
|
||||
ft.Add<StringId>(STR_STRING);
|
||||
ft.Add<const char*>(scenario->Details);
|
||||
ft.Add<const char*>(scenario->Details.c_str());
|
||||
screenPos.y += DrawTextWrapped(dpi, screenPos, 170, STR_BLACK_STRING, ft) + 5;
|
||||
|
||||
// Scenario objective
|
||||
@@ -445,13 +445,11 @@ namespace OpenRCT2::Ui::Windows
|
||||
bool isDisabled = listItem.scenario.is_locked;
|
||||
|
||||
// Draw scenario name
|
||||
char buffer[64];
|
||||
String::safeUtf8Copy(buffer, scenario->Name, sizeof(buffer));
|
||||
StringId format = isDisabled ? static_cast<StringId>(STR_STRINGID)
|
||||
: (isHighlighted ? highlighted_format : unhighlighted_format);
|
||||
auto ft = Formatter();
|
||||
ft.Add<StringId>(STR_STRING);
|
||||
ft.Add<char*>(buffer);
|
||||
ft.Add<const char*>(scenario->Name.c_str());
|
||||
auto colour = isDisabled ? colours[1].withFlag(ColourFlag::inset, true)
|
||||
: ColourWithFlags{ COLOUR_BLACK };
|
||||
auto darkness = isDisabled ? TextDarkness::Dark : TextDarkness::Regular;
|
||||
|
||||
@@ -215,15 +215,15 @@ namespace OpenRCT2
|
||||
|
||||
std::string name;
|
||||
ReadWriteStringTable(cs, name, "en-GB");
|
||||
String::set(entry.Name, sizeof(entry.Name), name.c_str());
|
||||
String::set(entry.InternalName, sizeof(entry.InternalName), name.c_str());
|
||||
entry.Name = name;
|
||||
entry.InternalName = name;
|
||||
|
||||
std::string parkName;
|
||||
ReadWriteStringTable(cs, parkName, "en-GB");
|
||||
|
||||
std::string scenarioDetails;
|
||||
ReadWriteStringTable(cs, scenarioDetails, "en-GB");
|
||||
String::set(entry.Details, sizeof(entry.Details), scenarioDetails.c_str());
|
||||
entry.Details = scenarioDetails;
|
||||
|
||||
entry.ObjectiveType = cs.Read<uint8_t>();
|
||||
entry.ObjectiveArg1 = cs.Read<uint8_t>();
|
||||
|
||||
@@ -262,7 +262,7 @@ namespace OpenRCT2::RCT1
|
||||
desc.title = name.c_str();
|
||||
}
|
||||
|
||||
String::set(dst->InternalName, sizeof(dst->InternalName), desc.title);
|
||||
dst->InternalName = desc.title;
|
||||
|
||||
if (!desc.textObjectId.empty())
|
||||
{
|
||||
@@ -284,8 +284,8 @@ namespace OpenRCT2::RCT1
|
||||
}
|
||||
}
|
||||
|
||||
String::set(dst->Name, sizeof(dst->Name), name.c_str());
|
||||
String::set(dst->Details, sizeof(dst->Details), details.c_str());
|
||||
dst->Name = name;
|
||||
dst->Details = details;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -257,18 +257,17 @@ namespace OpenRCT2::RCT2
|
||||
if (String::isNullOrEmpty(_s6.Info.Name))
|
||||
{
|
||||
// If the scenario doesn't have a name, set it to the filename
|
||||
String::set(dst->Name, sizeof(dst->Name), Path::GetFileNameWithoutExtension(dst->Path).c_str());
|
||||
dst->Name = Path::GetFileNameWithoutExtension(dst->Path);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Normalise the name to make the scenario as recognisable as possible.
|
||||
auto normalisedName = ScenarioSources::NormaliseName(_s6.Info.Name);
|
||||
String::set(dst->Name, sizeof(dst->Name), normalisedName.c_str());
|
||||
dst->Name = ScenarioSources::NormaliseName(_s6.Info.Name);
|
||||
}
|
||||
|
||||
// Look up and store information regarding the origins of this scenario.
|
||||
SourceDescriptor desc;
|
||||
if (ScenarioSources::TryGetByName(dst->Name, &desc))
|
||||
if (ScenarioSources::TryGetByName(dst->Name.c_str(), &desc))
|
||||
{
|
||||
dst->ScenarioId = desc.id;
|
||||
dst->SourceIndex = desc.index;
|
||||
@@ -290,8 +289,8 @@ namespace OpenRCT2::RCT2
|
||||
}
|
||||
|
||||
// dst->name will be translated later so keep the untranslated name here
|
||||
String::set(dst->InternalName, sizeof(dst->InternalName), dst->Name);
|
||||
String::set(dst->Details, sizeof(dst->Details), _s6.Info.Details);
|
||||
dst->InternalName = dst->Name;
|
||||
dst->Details = _s6.Info.Details;
|
||||
|
||||
if (!desc.textObjectId.empty())
|
||||
{
|
||||
@@ -308,11 +307,8 @@ namespace OpenRCT2::RCT2
|
||||
if (auto* obj = objManager.LoadObject(desc.textObjectId); obj != nullptr)
|
||||
{
|
||||
auto* textObject = reinterpret_cast<ScenarioTextObject*>(obj);
|
||||
auto name = textObject->GetScenarioName();
|
||||
auto details = textObject->GetScenarioDetails();
|
||||
|
||||
String::set(dst->Name, sizeof(dst->Name), name.c_str());
|
||||
String::set(dst->Details, sizeof(dst->Details), details.c_str());
|
||||
dst->Name = textObject->GetScenarioName();
|
||||
dst->Details = textObject->GetScenarioDetails();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -71,10 +71,10 @@ static int32_t ScenarioIndexEntryCompareByCategory(const ScenarioIndexEntry& ent
|
||||
{
|
||||
return static_cast<int32_t>(entryA.SourceGame) - static_cast<int32_t>(entryB.SourceGame);
|
||||
}
|
||||
return strcmp(entryA.Name, entryB.Name);
|
||||
return strcmp(entryA.Name.c_str(), entryB.Name.c_str());
|
||||
case SCENARIO_CATEGORY_REAL:
|
||||
case SCENARIO_CATEGORY_OTHER:
|
||||
return strcmp(entryA.Name, entryB.Name);
|
||||
return strcmp(entryA.Name.c_str(), entryB.Name.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,7 +317,7 @@ public:
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const ScenarioIndexEntry* GetByInternalName(const utf8* name) const override
|
||||
const ScenarioIndexEntry* GetByInternalName(u8string_view name) const override
|
||||
{
|
||||
for (size_t i = 0; i < _scenarios.size(); i++)
|
||||
{
|
||||
|
||||
@@ -58,9 +58,9 @@ struct ScenarioIndexEntry
|
||||
int16_t ObjectiveArg3;
|
||||
ScenarioHighscoreEntry* Highscore = nullptr;
|
||||
|
||||
utf8 InternalName[64]; // Untranslated name
|
||||
utf8 Name[64]; // Translated name
|
||||
utf8 Details[256];
|
||||
u8string InternalName; // Untranslated name
|
||||
u8string Name; // Translated name
|
||||
u8string Details;
|
||||
};
|
||||
|
||||
namespace OpenRCT2
|
||||
@@ -83,7 +83,7 @@ struct IScenarioRepository
|
||||
/**
|
||||
* Does not return custom scenarios due to the fact that they may have the same name.
|
||||
*/
|
||||
virtual const ScenarioIndexEntry* GetByInternalName(const utf8* name) const = 0;
|
||||
virtual const ScenarioIndexEntry* GetByInternalName(u8string_view name) const = 0;
|
||||
virtual const ScenarioIndexEntry* GetByPath(const utf8* path) const = 0;
|
||||
|
||||
virtual bool TryRecordHighscore(int32_t language, const utf8* scenarioFileName, money64 companyValue, const utf8* name) = 0;
|
||||
|
||||
@@ -357,7 +357,7 @@ namespace OpenRCT2::ScenarioSources
|
||||
|
||||
#pragma endregion
|
||||
|
||||
bool TryGetByName(const utf8* name, SourceDescriptor* outDesc)
|
||||
bool TryGetByName(u8string_view name, SourceDescriptor* outDesc)
|
||||
{
|
||||
Guard::ArgumentNotNull(outDesc, GUARD_LINE);
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ struct SourceDescriptor
|
||||
|
||||
namespace OpenRCT2::ScenarioSources
|
||||
{
|
||||
bool TryGetByName(const utf8* name, SourceDescriptor* outDesc);
|
||||
bool TryGetByName(u8string_view name, SourceDescriptor* outDesc);
|
||||
bool TryGetById(uint8_t id, SourceDescriptor* outDesc);
|
||||
u8string NormaliseName(u8string_view input);
|
||||
} // namespace OpenRCT2::ScenarioSources
|
||||
|
||||
Reference in New Issue
Block a user