1
0
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:
Briar
2025-02-14 15:18:24 -08:00
committed by GitHub
parent 503d55d051
commit 376cb7980c
10 changed files with 27 additions and 31 deletions

View File

@@ -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

View File

@@ -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.

View File

@@ -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;

View File

@@ -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>();

View File

@@ -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;
}

View File

@@ -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();
}
}

View File

@@ -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++)
{

View File

@@ -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;

View File

@@ -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);

View File

@@ -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