/***************************************************************************** * Copyright (c) 2014-2020 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 "LanguagePack.h" #include "../common.h" #include "../core/FileStream.h" #include "../core/Memory.hpp" #include "../core/RTL.h" #include "../core/String.hpp" #include "../core/StringBuilder.h" #include "../core/StringReader.h" #include "Language.h" #include "Localisation.h" #include #include #include #include // Don't try to load more than language files that exceed 64 MiB constexpr uint64_t MAX_LANGUAGE_SIZE = 64 * 1024 * 1024; constexpr uint64_t MAX_OBJECT_OVERRIDES = 4096; constexpr uint64_t MAX_SCENARIO_OVERRIDES = 4096; constexpr rct_string_id ObjectOverrideBase = 0x6000; constexpr int32_t ObjectOverrideMaxStringCount = 3; constexpr rct_string_id ScenarioOverrideBase = 0x7000; constexpr int32_t ScenarioOverrideMaxStringCount = 3; struct ObjectOverride { char name[8] = { 0 }; std::string strings[ObjectOverrideMaxStringCount]; }; struct ScenarioOverride { std::string filename; std::string strings[ScenarioOverrideMaxStringCount]; }; class LanguagePack final : public ILanguagePack { private: uint16_t const _id; std::vector _strings; std::vector _objectOverrides; std::vector _scenarioOverrides; /////////////////////////////////////////////////////////////////////////// // Parsing work data /////////////////////////////////////////////////////////////////////////// std::string _currentGroup; ObjectOverride* _currentObjectOverride = nullptr; ScenarioOverride* _currentScenarioOverride = nullptr; public: static std::unique_ptr FromFile(uint16_t id, const utf8* path) { Guard::ArgumentNotNull(path); // Load file directly into memory utf8* fileData = nullptr; try { OpenRCT2::FileStream fs = OpenRCT2::FileStream(path, OpenRCT2::FILE_MODE_OPEN); size_t fileLength = static_cast(fs.GetLength()); if (fileLength > MAX_LANGUAGE_SIZE) { throw IOException("Language file too large."); } fileData = Memory::Allocate(fileLength + 1); fs.Read(fileData, fileLength); fileData[fileLength] = '\0'; } catch (const std::exception& ex) { Memory::Free(fileData); log_error("Unable to open %s: %s", path, ex.what()); return nullptr; } // Parse the memory as text auto result = FromText(id, fileData); Memory::Free(fileData); return result; } static std::unique_ptr FromText(uint16_t id, const utf8* text) { return std::make_unique(id, text); } LanguagePack(uint16_t id, const utf8* text) : _id(id) { Guard::ArgumentNotNull(text); auto reader = UTF8StringReader(text); while (reader.CanRead()) { ParseLine(&reader); } // Clean up the parsing work data _currentGroup.clear(); _currentObjectOverride = nullptr; _currentScenarioOverride = nullptr; } uint16_t GetId() const override { return _id; } uint32_t GetCount() const override { return static_cast(_strings.size()); } void RemoveString(rct_string_id stringId) override { if (_strings.size() > static_cast(stringId)) { _strings[stringId].clear(); } } void SetString(rct_string_id stringId, const std::string& str) override { if (_strings.size() > static_cast(stringId)) { _strings[stringId] = str; } } const utf8* GetString(rct_string_id stringId) const override { if (stringId >= ScenarioOverrideBase) { int32_t offset = stringId - ScenarioOverrideBase; int32_t ooIndex = offset / ScenarioOverrideMaxStringCount; int32_t ooStringIndex = offset % ScenarioOverrideMaxStringCount; if (_scenarioOverrides.size() > static_cast(ooIndex) && !_scenarioOverrides[ooIndex].strings[ooStringIndex].empty()) { return _scenarioOverrides[ooIndex].strings[ooStringIndex].c_str(); } return nullptr; } if (stringId >= ObjectOverrideBase) { int32_t offset = stringId - ObjectOverrideBase; int32_t ooIndex = offset / ObjectOverrideMaxStringCount; int32_t ooStringIndex = offset % ObjectOverrideMaxStringCount; if (_objectOverrides.size() > static_cast(ooIndex) && !_objectOverrides[ooIndex].strings[ooStringIndex].empty()) { return _objectOverrides[ooIndex].strings[ooStringIndex].c_str(); } return nullptr; } if ((_strings.size() > static_cast(stringId)) && !_strings[stringId].empty()) { return _strings[stringId].c_str(); } return nullptr; } rct_string_id GetObjectOverrideStringId(std::string_view legacyIdentifier, uint8_t index) override { Guard::Assert(index < ObjectOverrideMaxStringCount); int32_t ooIndex = 0; for (const ObjectOverride& objectOverride : _objectOverrides) { if (std::string_view(objectOverride.name, 8) == legacyIdentifier) { if (objectOverride.strings[index].empty()) { return STR_NONE; } return ObjectOverrideBase + (ooIndex * ObjectOverrideMaxStringCount) + index; } ooIndex++; } return STR_NONE; } rct_string_id GetScenarioOverrideStringId(const utf8* scenarioFilename, uint8_t index) override { Guard::ArgumentNotNull(scenarioFilename); Guard::Assert(index < ScenarioOverrideMaxStringCount); int32_t ooIndex = 0; for (const ScenarioOverride& scenarioOverride : _scenarioOverrides) { if (String::Equals(scenarioOverride.filename.c_str(), scenarioFilename, true)) { if (scenarioOverride.strings[index].empty()) { return STR_NONE; } return ScenarioOverrideBase + (ooIndex * ScenarioOverrideMaxStringCount) + index; } ooIndex++; } return STR_NONE; } private: ObjectOverride* GetObjectOverride(const std::string& objectIdentifier) { for (auto& oo : _objectOverrides) { if (strncmp(oo.name, objectIdentifier.c_str(), 8) == 0) { return &oo; } } return nullptr; } ScenarioOverride* GetScenarioOverride(const std::string& scenarioIdentifier) { for (auto& so : _scenarioOverrides) { if (String::Equals(so.strings[0], scenarioIdentifier.c_str(), true)) { return &so; } } return nullptr; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Parsing //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Partial support to open an uncompiled language file which parses tokens and converts them to the corresponding character // code. Due to resource strings (strings in scenarios and objects) being written to the original game's string table, // get_string will use those if the same entry in the loaded language is empty. // // Unsure at how the original game decides which entries to write resource strings to, but this could affect adding new // strings for the time being. Further investigation is required. // // When reading the language files, the STR_XXXX part is read and XXXX becomes the string id number. Everything after the // colon and before the new line will be saved as the string. Tokens are written with inside curly braces {TOKEN}. Use # at // the beginning of a line to leave a comment. //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// static bool IsWhitespace(codepoint_t codepoint) { return codepoint == '\t' || codepoint == ' ' || codepoint == '\r' || codepoint == '\n'; } static bool IsNewLine(codepoint_t codepoint) { return codepoint == '\r' || codepoint == '\n'; } static void SkipWhitespace(IStringReader* reader) { codepoint_t codepoint; while (reader->TryPeek(&codepoint)) { if (IsWhitespace(codepoint)) { reader->Skip(); } else { break; } } } static void SkipNewLine(IStringReader* reader) { codepoint_t codepoint; while (reader->TryPeek(&codepoint)) { if (IsNewLine(codepoint)) { reader->Skip(); } else { break; } } } static void SkipToEndOfLine(IStringReader* reader) { codepoint_t codepoint; while (reader->TryPeek(&codepoint)) { if (codepoint != '\r' && codepoint != '\n') { reader->Skip(); } else { break; } } } void ParseLine(IStringReader* reader) { SkipWhitespace(reader); codepoint_t codepoint; if (reader->TryPeek(&codepoint)) { switch (codepoint) { case '#': SkipToEndOfLine(reader); break; case '[': ParseGroupObject(reader); break; case '<': ParseGroupScenario(reader); break; case '\r': case '\n': break; default: ParseString(reader); break; } SkipToEndOfLine(reader); SkipNewLine(reader); } } void ParseGroupObject(IStringReader* reader) { auto sb = StringBuilder(); codepoint_t codepoint; // Should have already deduced that the next codepoint is a [ reader->Skip(); // Read string up to ] or line end bool closedCorrectly = false; while (reader->TryPeek(&codepoint)) { if (IsNewLine(codepoint)) break; reader->Skip(); if (codepoint == ']') { closedCorrectly = true; break; } sb.Append(codepoint); } if (closedCorrectly) { while (sb.GetLength() < 8) { sb.Append(' '); } if (sb.GetLength() == 8) { _currentGroup = sb.GetStdString(); _currentObjectOverride = GetObjectOverride(_currentGroup); _currentScenarioOverride = nullptr; if (_currentObjectOverride == nullptr) { if (_objectOverrides.size() == MAX_OBJECT_OVERRIDES) { log_warning("Maximum number of localised object strings exceeded."); } _objectOverrides.emplace_back(); _currentObjectOverride = &_objectOverrides[_objectOverrides.size() - 1]; std::copy_n(_currentGroup.c_str(), 8, _currentObjectOverride->name); } } } } void ParseGroupScenario(IStringReader* reader) { auto sb = StringBuilder(); codepoint_t codepoint; // Should have already deduced that the next codepoint is a < reader->Skip(); // Read string up to > or line end bool closedCorrectly = false; while (reader->TryPeek(&codepoint)) { if (IsNewLine(codepoint)) break; reader->Skip(); if (codepoint == '>') { closedCorrectly = true; break; } sb.Append(codepoint); } if (closedCorrectly) { _currentGroup = sb.GetStdString(); _currentObjectOverride = nullptr; _currentScenarioOverride = GetScenarioOverride(_currentGroup); if (_currentScenarioOverride == nullptr) { if (_scenarioOverrides.size() == MAX_SCENARIO_OVERRIDES) { log_warning("Maximum number of scenario strings exceeded."); } _scenarioOverrides.emplace_back(); _currentScenarioOverride = &_scenarioOverrides[_scenarioOverrides.size() - 1]; _currentScenarioOverride->filename = std::string(sb.GetBuffer()); } } } void ParseString(IStringReader* reader) { auto sb = StringBuilder(); codepoint_t codepoint; // Parse string identifier while (reader->TryPeek(&codepoint)) { if (IsNewLine(codepoint)) { // Unexpected new line, ignore line entirely return; } if (!IsWhitespace(codepoint) && codepoint != ':') { reader->Skip(); sb.Append(codepoint); } else { break; } } SkipWhitespace(reader); // Parse a colon if (!reader->TryPeek(&codepoint) || codepoint != ':') { // Expected a colon, ignore line entirely return; } reader->Skip(); // Validate identifier const utf8* identifier = sb.GetBuffer(); int32_t stringId; if (_currentGroup.empty()) { if (sscanf(identifier, "STR_%4d", &stringId) != 1) { // Ignore line entirely return; } } else { if (String::Equals(identifier, "STR_NAME")) { stringId = 0; } else if (String::Equals(identifier, "STR_DESC")) { stringId = 1; } else if (String::Equals(identifier, "STR_CPTY")) { stringId = 2; } else if (String::Equals(identifier, "STR_SCNR")) { stringId = 0; } else if (String::Equals(identifier, "STR_PARK")) { stringId = 1; } else if (String::Equals(identifier, "STR_DTLS")) { stringId = 2; } else { // Ignore line entirely return; } } // Rest of the line is the actual string sb.Clear(); while (reader->TryPeek(&codepoint) && !IsNewLine(codepoint)) { reader->Skip(); sb.Append(codepoint); } std::string s; if (LanguagesDescriptors[_id].isRtl) { auto ts = std::string(sb.GetBuffer(), sb.GetLength()); s = FixRTL(ts); } else { s = std::string(sb.GetBuffer(), sb.GetLength()); } if (_currentGroup.empty()) { // Make sure the list is big enough to contain this string id if (static_cast(stringId) >= _strings.size()) { _strings.resize(stringId + 1); } _strings[stringId] = s; } else { if (_currentObjectOverride != nullptr) { _currentObjectOverride->strings[stringId] = s; } else { _currentScenarioOverride->strings[stringId] = std::move(s); } } } }; namespace LanguagePackFactory { std::unique_ptr FromFile(uint16_t id, const utf8* path) { auto languagePack = LanguagePack::FromFile(id, path); return languagePack; } std::unique_ptr FromText(uint16_t id, const utf8* text) { auto languagePack = LanguagePack::FromText(id, text); return languagePack; } } // namespace LanguagePackFactory