diff --git a/src/localisation/LanguagePack.cpp b/src/localisation/LanguagePack.cpp index 587928b5b3..af5b439f30 100644 --- a/src/localisation/LanguagePack.cpp +++ b/src/localisation/LanguagePack.cpp @@ -14,11 +14,12 @@ *****************************************************************************/ #pragma endregion +#include +#include + extern "C" { #include "../common.h" - #include "../platform/platform.h" - #include "../util/util.h" #include "localisation.h" } @@ -27,8 +28,8 @@ extern "C" #include "../core/Memory.hpp" #include "../core/String.hpp" #include "../core/StringBuilder.hpp" +#include "../core/StringReader.hpp" #include "LanguagePack.h" -#include // Don't try to load more than language files that exceed 64 MiB constexpr uint64 MAX_LANGUAGE_SIZE = 64 * 1024 * 1024; @@ -41,582 +42,659 @@ constexpr int ObjectOverrideMaxStringCount = 4; constexpr rct_string_id ScenarioOverrideBase = 0x7000; constexpr int ScenarioOverrideMaxStringCount = 3; -LanguagePack * LanguagePack::FromFile(uint16 id, const utf8 * path) +struct ObjectOverride { - Guard::ArgumentNotNull(path); + char name[8]; + const utf8 * strings[4]; +}; - // Load file directly into memory - utf8 * fileData = nullptr; - try +struct ScenarioOverride +{ + std::string filename; + union { + const utf8 * strings[3]; + struct { + const utf8 * name; + const utf8 * park; + const utf8 * details; + }; + }; +}; + +class LanguagePack : public ILanguagePack +{ +private: + uint16 _id; + utf8 * _stringData; + + std::vector _strings; + std::vector _objectOverrides; + std::vector _scenarioOverrides; + + /////////////////////////////////////////////////////////////////////////// + // Parsing work data + /////////////////////////////////////////////////////////////////////////// + StringBuilder _stringDataSB; + utf8 * _currentGroup; + ObjectOverride * _currentObjectOverride; + ScenarioOverride * _currentScenarioOverride; + +public: + static LanguagePack * FromFile(uint16 id, const utf8 * path) { - FileStream fs = FileStream(path, FILE_MODE_OPEN); + Guard::ArgumentNotNull(path); + + // Load file directly into memory + utf8 * fileData = nullptr; + try + { + FileStream fs = FileStream(path, FILE_MODE_OPEN); - size_t fileLength = (size_t)fs.GetLength(); - if (fileLength > MAX_LANGUAGE_SIZE) + size_t fileLength = (size_t)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 (Exception ex) { - throw IOException("Language file too large."); + Memory::Free(fileData); + log_error("Unable to open %s: %s", path, ex.GetMessage()); + return nullptr; } - fileData = Memory::Allocate(fileLength + 1); - fs.Read(fileData, fileLength); - fileData[fileLength] = '\0'; - } - catch (Exception ex) - { + // Parse the memory as text + LanguagePack * result = FromText(id, fileData); + Memory::Free(fileData); - log_error("Unable to open %s: %s", path, ex.GetMessage()); - return nullptr; + return result; } - // Parse the memory as text - LanguagePack * result = FromText(id, fileData); - - Memory::Free(fileData); - return result; -} - -LanguagePack * LanguagePack::FromText(uint16 id, const utf8 * text) -{ - return new LanguagePack(id, text); -} - -LanguagePack::LanguagePack(uint16 id, const utf8 * text) -{ - Guard::ArgumentNotNull(text); - - _id = id; - _stringData = nullptr; - _currentGroup = nullptr; - _currentObjectOverride = nullptr; - _currentScenarioOverride = nullptr; - - auto reader = UTF8StringReader(text); - while (reader.CanRead()) + static LanguagePack * FromText(uint16 id, const utf8 * text) { - ParseLine(&reader); + return new LanguagePack(id, text); } - _stringData = _stringDataSB.GetString(); - - size_t stringDataBaseAddress = (size_t)_stringData; - for (size_t i = 0; i < _strings.size(); i++) + LanguagePack(uint16 id, const utf8 * text) { - if (_strings[i] != nullptr) - { - _strings[i] = (utf8*)(stringDataBaseAddress + (size_t)_strings[i]); - } - } - for (size_t i = 0; i < _objectOverrides.size(); i++) - { - for (int j = 0; j < ObjectOverrideMaxStringCount; j++) - { - const utf8 * * strPtr = &(_objectOverrides[i].strings[j]); - if (*strPtr != nullptr) - { - *strPtr = (utf8*)(stringDataBaseAddress + (size_t)*strPtr); - } - } - } - for (size_t i = 0; i < _scenarioOverrides.size(); i++) - { - for (int j = 0; j < ScenarioOverrideMaxStringCount; j++) - { - const utf8 **strPtr = &(_scenarioOverrides[i].strings[j]); - if (*strPtr != nullptr) - { - *strPtr = (utf8*)(stringDataBaseAddress + (size_t)*strPtr); - } - } - } + Guard::ArgumentNotNull(text); - // Destruct the string builder to free memory - _stringDataSB = StringBuilder(); -} - -LanguagePack::~LanguagePack() -{ - Memory::Free(_stringData); - Memory::Free(_currentGroup); -} - -const utf8 * LanguagePack::GetString(rct_string_id stringId) const -{ - if (stringId >= ScenarioOverrideBase) - { - int offset = stringId - ScenarioOverrideBase; - int ooIndex = offset / ScenarioOverrideMaxStringCount; - int ooStringIndex = offset % ScenarioOverrideMaxStringCount; - - if (_scenarioOverrides.size() > (size_t)ooIndex) - { - return _scenarioOverrides[ooIndex].strings[ooStringIndex]; - } - else - { - return nullptr; - } - } - else if (stringId >= ObjectOverrideBase) - { - int offset = stringId - ObjectOverrideBase; - int ooIndex = offset / ObjectOverrideMaxStringCount; - int ooStringIndex = offset % ObjectOverrideMaxStringCount; - - if (_objectOverrides.size() > (size_t)ooIndex) - { - return _objectOverrides[ooIndex].strings[ooStringIndex]; - } - else - { - return nullptr; - } - } - else - { - if (_strings.size() > (size_t)stringId) - { - return _strings[stringId]; - } - else - { - return nullptr; - } - } -} - -rct_string_id LanguagePack::GetObjectOverrideStringId(const char * objectIdentifier, uint8 index) -{ - Guard::ArgumentNotNull(objectIdentifier); - Guard::Assert(index < ObjectOverrideMaxStringCount); - - int ooIndex = 0; - for (const ObjectOverride &objectOverride : _objectOverrides) - { - if (strncmp(objectOverride.name, objectIdentifier, 8) == 0) - { - if (objectOverride.strings[index] == nullptr) - { - return STR_NONE; - } - return ObjectOverrideBase + (ooIndex * ObjectOverrideMaxStringCount) + index; - } - ooIndex++; - } - - return STR_NONE; -} - -rct_string_id LanguagePack::GetScenarioOverrideStringId(const utf8 * scenarioFilename, uint8 index) -{ - Guard::ArgumentNotNull(scenarioFilename); - Guard::Assert(index < ScenarioOverrideMaxStringCount); - - int ooIndex = 0; - for (const ScenarioOverride &scenarioOverride : _scenarioOverrides) - { - if (String::Equals(scenarioOverride.filename.c_str(), scenarioFilename, true)) - { - if (scenarioOverride.strings[index] == nullptr) - { - return STR_NONE; - } - return ScenarioOverrideBase + (ooIndex * ScenarioOverrideMaxStringCount) + index; - } - ooIndex++; - } - - return STR_NONE; -} - -LanguagePack::ObjectOverride * LanguagePack::GetObjectOverride(const char * objectIdentifier) -{ - Guard::ArgumentNotNull(objectIdentifier); - - for (size_t i = 0; i < _objectOverrides.size(); i++) - { - ObjectOverride *oo = &_objectOverrides[i]; - if (strncmp(oo->name, objectIdentifier, 8) == 0) - { - return oo; - } - } - return nullptr; -} - -LanguagePack::ScenarioOverride * LanguagePack::GetScenarioOverride(const utf8 * scenarioIdentifier) -{ - Guard::ArgumentNotNull(scenarioIdentifier); - - for (size_t i = 0; i < _scenarioOverrides.size(); i++) - { - ScenarioOverride *so = &_scenarioOverrides[i]; - // At this point ScenarioOverrides were not yet rewritten to point at - // strings, but rather still hold offsets from base. - const utf8 *name = _stringDataSB.GetBuffer() + (size_t)so->name; - if (_stricmp(name, scenarioIdentifier) == 0) - { - return so; - } - } - return nullptr; -} - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// Parsing -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// Partial support to open a 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 LanguagePack::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 LanguagePack::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) - { - SafeFree(_currentGroup); - - while (sb.GetLength() < 8) - { - sb.Append(' '); - } - if (sb.GetLength() == 8) - { - _currentGroup = sb.GetString(); - _currentObjectOverride = GetObjectOverride(_currentGroup); - _currentScenarioOverride = nullptr; - if (_currentObjectOverride == nullptr) - { - if (_objectOverrides.size() == MAX_OBJECT_OVERRIDES) - { - log_warning("Maximum number of localised object strings exceeded."); - } - - _objectOverrides.push_back(ObjectOverride()); - _currentObjectOverride = &_objectOverrides[_objectOverrides.size() - 1]; - memset(_currentObjectOverride, 0, sizeof(ObjectOverride)); - memcpy(_currentObjectOverride->name, _currentGroup, 8); - } - } - } -} - -void LanguagePack::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) - { - SafeFree(_currentGroup); - - _currentGroup = sb.GetString(); + _id = id; + _stringData = nullptr; + _currentGroup = nullptr; _currentObjectOverride = nullptr; - _currentScenarioOverride = GetScenarioOverride(_currentGroup); - if (_currentScenarioOverride == nullptr) + _currentScenarioOverride = nullptr; + + auto reader = UTF8StringReader(text); + while (reader.CanRead()) { - if (_scenarioOverrides.size() == MAX_SCENARIO_OVERRIDES) + ParseLine(&reader); + } + + _stringData = _stringDataSB.GetString(); + + size_t stringDataBaseAddress = (size_t)_stringData; + for (size_t i = 0; i < _strings.size(); i++) + { + if (_strings[i] != nullptr) { - log_warning("Maximum number of scenario strings exceeded."); + _strings[i] = (utf8*)(stringDataBaseAddress + (size_t)_strings[i]); } - - _scenarioOverrides.push_back(ScenarioOverride()); - _currentScenarioOverride = &_scenarioOverrides[_scenarioOverrides.size() - 1]; - Memory::Set(_currentScenarioOverride, 0, sizeof(ScenarioOverride)); - _currentScenarioOverride->filename = std::string(sb.GetBuffer()); } - } -} - -void LanguagePack::ParseString(IStringReader *reader) -{ - auto sb = StringBuilder(); - codepoint_t codepoint; - - // Parse string identifier - while (reader->TryPeek(&codepoint)) - { - if (IsNewLine(codepoint)) + for (size_t i = 0; i < _objectOverrides.size(); i++) { - // Unexpected new line, ignore line entirely - return; - } - else 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(); - - int stringId; - if (_currentGroup == nullptr) - { - 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)) - { - if (codepoint == '{') - { - uint32 token; - bool isByte; - if (ParseToken(reader, &token, &isByte)) + for (int j = 0; j < ObjectOverrideMaxStringCount; j++) { - if (isByte) + const utf8 * * strPtr = &(_objectOverrides[i].strings[j]); + if (*strPtr != nullptr) { - sb.Append((const utf8*)&token, 1); + *strPtr = (utf8*)(stringDataBaseAddress + (size_t)*strPtr); } - else + } + } + for (size_t i = 0; i < _scenarioOverrides.size(); i++) + { + for (int j = 0; j < ScenarioOverrideMaxStringCount; j++) + { + const utf8 **strPtr = &(_scenarioOverrides[i].strings[j]); + if (*strPtr != nullptr) { - sb.Append((int)token); + *strPtr = (utf8*)(stringDataBaseAddress + (size_t)*strPtr); } } + } + + // Clean up the parsing work data + Memory::Free(_currentGroup); + // Destruct the string builder to free memory + _stringDataSB = StringBuilder(); + _currentGroup = nullptr; + _currentObjectOverride = nullptr; + _currentScenarioOverride = nullptr; + } + + ~LanguagePack() + { + Memory::Free(_stringData); + Memory::Free(_currentGroup); + } + + uint16 GetId() const override + { + return _id; + } + + uint32 GetCount() const override + { + return (uint32)_strings.size(); + } + + void SetString(rct_string_id stringId, const utf8 * str) override + { + if (_strings.size() >= (size_t)stringId) + { + _strings[stringId] = str; + } + } + + const utf8 * GetString(rct_string_id stringId) const override + { + if (stringId >= ScenarioOverrideBase) + { + int offset = stringId - ScenarioOverrideBase; + int ooIndex = offset / ScenarioOverrideMaxStringCount; + int ooStringIndex = offset % ScenarioOverrideMaxStringCount; + + if (_scenarioOverrides.size() > (size_t)ooIndex) + { + return _scenarioOverrides[ooIndex].strings[ooStringIndex]; + } else { - // Syntax error or unknown token, ignore line entirely + return nullptr; + } + } + else if (stringId >= ObjectOverrideBase) + { + int offset = stringId - ObjectOverrideBase; + int ooIndex = offset / ObjectOverrideMaxStringCount; + int ooStringIndex = offset % ObjectOverrideMaxStringCount; + + if (_objectOverrides.size() > (size_t)ooIndex) + { + return _objectOverrides[ooIndex].strings[ooStringIndex]; + } + else + { + return nullptr; + } + } + else + { + if (_strings.size() > (size_t)stringId) + { + return _strings[stringId]; + } + else + { + return nullptr; + } + } + } + + rct_string_id GetObjectOverrideStringId(const char * objectIdentifier, uint8 index) override + { + Guard::ArgumentNotNull(objectIdentifier); + Guard::Assert(index < ObjectOverrideMaxStringCount); + + int ooIndex = 0; + for (const ObjectOverride &objectOverride : _objectOverrides) + { + if (strncmp(objectOverride.name, objectIdentifier, 8) == 0) + { + if (objectOverride.strings[index] == nullptr) + { + return STR_NONE; + } + return ObjectOverrideBase + (ooIndex * ObjectOverrideMaxStringCount) + index; + } + ooIndex++; + } + + return STR_NONE; + } + + rct_string_id GetScenarioOverrideStringId(const utf8 * scenarioFilename, uint8 index) override + { + Guard::ArgumentNotNull(scenarioFilename); + Guard::Assert(index < ScenarioOverrideMaxStringCount); + + int ooIndex = 0; + for (const ScenarioOverride &scenarioOverride : _scenarioOverrides) + { + if (String::Equals(scenarioOverride.filename.c_str(), scenarioFilename, true)) + { + if (scenarioOverride.strings[index] == nullptr) + { + return STR_NONE; + } + return ScenarioOverrideBase + (ooIndex * ScenarioOverrideMaxStringCount) + index; + } + ooIndex++; + } + + return STR_NONE; + } + + ObjectOverride * GetObjectOverride(const char * objectIdentifier) + { + Guard::ArgumentNotNull(objectIdentifier); + + for (size_t i = 0; i < _objectOverrides.size(); i++) + { + ObjectOverride *oo = &_objectOverrides[i]; + if (strncmp(oo->name, objectIdentifier, 8) == 0) + { + return oo; + } + } + return nullptr; + } + + ScenarioOverride * GetScenarioOverride(const utf8 * scenarioIdentifier) + { + Guard::ArgumentNotNull(scenarioIdentifier); + + for (size_t i = 0; i < _scenarioOverrides.size(); i++) + { + ScenarioOverride *so = &_scenarioOverrides[i]; + // At this point ScenarioOverrides were not yet rewritten to point at + // strings, but rather still hold offsets from base. + const utf8 *name = _stringDataSB.GetBuffer() + (size_t)so->name; + if (_stricmp(name, scenarioIdentifier) == 0) + { + return so; + } + } + return nullptr; + } + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Parsing + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Partial support to open a 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) + { + SafeFree(_currentGroup); + + while (sb.GetLength() < 8) + { + sb.Append(' '); + } + if (sb.GetLength() == 8) + { + _currentGroup = sb.GetString(); + _currentObjectOverride = GetObjectOverride(_currentGroup); + _currentScenarioOverride = nullptr; + if (_currentObjectOverride == nullptr) + { + if (_objectOverrides.size() == MAX_OBJECT_OVERRIDES) + { + log_warning("Maximum number of localised object strings exceeded."); + } + + _objectOverrides.push_back(ObjectOverride()); + _currentObjectOverride = &_objectOverrides[_objectOverrides.size() - 1]; + memset(_currentObjectOverride, 0, sizeof(ObjectOverride)); + memcpy(_currentObjectOverride->name, _currentGroup, 8); + } + } + } + } + + 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) + { + SafeFree(_currentGroup); + + _currentGroup = sb.GetString(); + _currentObjectOverride = nullptr; + _currentScenarioOverride = GetScenarioOverride(_currentGroup); + if (_currentScenarioOverride == nullptr) + { + if (_scenarioOverrides.size() == MAX_SCENARIO_OVERRIDES) + { + log_warning("Maximum number of scenario strings exceeded."); + } + + _scenarioOverrides.push_back(ScenarioOverride()); + _currentScenarioOverride = &_scenarioOverrides[_scenarioOverrides.size() - 1]; + Memory::Set(_currentScenarioOverride, 0, sizeof(ScenarioOverride)); + _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; + } + else 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(); + + int stringId; + if (_currentGroup == nullptr) + { + if (sscanf(identifier, "STR_%4d", &stringId) != 1) + { + // Ignore line entirely return; } } else { - reader->Skip(); - sb.Append(codepoint); - } - } + 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; } - // Append a null terminator for the benefit of the last string - _stringDataSB.Append('\0'); - - // Get the relative offset to the string (add the base offset when we extract the string properly) - utf8 * relativeOffset = (utf8*)_stringDataSB.GetLength(); - - if (_currentGroup == nullptr) - { - // Make sure the list is big enough to contain this string id - while (_strings.size() <= (size_t)stringId) - { - _strings.push_back(nullptr); + 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; + } } - _strings[stringId] = relativeOffset; - } - else - { - if (_currentObjectOverride != nullptr) + // Rest of the line is the actual string + sb.Clear(); + while (reader->TryPeek(&codepoint) && !IsNewLine(codepoint)) { - _currentObjectOverride->strings[stringId] = relativeOffset; + if (codepoint == '{') + { + uint32 token; + bool isByte; + if (ParseToken(reader, &token, &isByte)) + { + if (isByte) + { + sb.Append((const utf8*)&token, 1); + } + else + { + sb.Append((int)token); + } + } + else + { + // Syntax error or unknown token, ignore line entirely + return; + } + } + else + { + reader->Skip(); + sb.Append(codepoint); + } + } + + // Append a null terminator for the benefit of the last string + _stringDataSB.Append('\0'); + + // Get the relative offset to the string (add the base offset when we extract the string properly) + utf8 * relativeOffset = (utf8*)_stringDataSB.GetLength(); + + if (_currentGroup == nullptr) + { + // Make sure the list is big enough to contain this string id + while (_strings.size() <= (size_t)stringId) + { + _strings.push_back(nullptr); + } + + _strings[stringId] = relativeOffset; } else { - _currentScenarioOverride->strings[stringId] = relativeOffset; + if (_currentObjectOverride != nullptr) + { + _currentObjectOverride->strings[stringId] = relativeOffset; + } + else + { + _currentScenarioOverride->strings[stringId] = relativeOffset; + } } + + _stringDataSB.Append(&sb); } - _stringDataSB.Append(&sb); -} - -bool LanguagePack::ParseToken(IStringReader * reader, uint32 * token, bool * isByte) -{ - auto sb = StringBuilder(); - codepoint_t codepoint; - - // Skip open brace - reader->Skip(); - - while (reader->TryPeek(&codepoint)) + bool ParseToken(IStringReader * reader, uint32 * token, bool * isByte) { - if (IsNewLine(codepoint)) return false; - if (IsWhitespace(codepoint)) return false; + auto sb = StringBuilder(); + codepoint_t codepoint; + // Skip open brace reader->Skip(); - if (codepoint == '}') break; - - sb.Append(codepoint); - } - - const utf8 * tokenName = sb.GetBuffer(); - *token = format_get_code(tokenName); - *isByte = false; - - // Handle explicit byte values - if (*token == 0) - { - int number; - if (sscanf(tokenName, "%d", &number) == 1) + while (reader->TryPeek(&codepoint)) { - *token = Math::Clamp(0, number, 255); - *isByte = true; + if (IsNewLine(codepoint)) return false; + if (IsWhitespace(codepoint)) return false; + + reader->Skip(); + + if (codepoint == '}') break; + + sb.Append(codepoint); } + + const utf8 * tokenName = sb.GetBuffer(); + *token = format_get_code(tokenName); + *isByte = false; + + // Handle explicit byte values + if (*token == 0) + { + int number; + if (sscanf(tokenName, "%d", &number) == 1) + { + *token = Math::Clamp(0, number, 255); + *isByte = true; + } + } + + return true; + } +}; + +namespace LanguagePackFactory +{ + ILanguagePack * FromFile(uint16 id, const utf8 * path) + { + auto languagePack = LanguagePack::FromFile(id, path); + return languagePack; } - return true; + ILanguagePack * FromText(uint16 id, const utf8 * text) + { + auto languagePack = LanguagePack::FromText(id, text); + return languagePack; + } } diff --git a/src/localisation/LanguagePack.h b/src/localisation/LanguagePack.h index 1e8108f81c..2c83a0dd08 100644 --- a/src/localisation/LanguagePack.h +++ b/src/localisation/LanguagePack.h @@ -16,86 +16,23 @@ #pragma once -#include -#include +#include "../common.h" -extern "C" +interface ILanguagePack { - #include "../common.h" - #include "../util/util.h" - #include "localisation.h" -} + virtual ~ILanguagePack() = default; -#include "../core/StringBuilder.hpp" -#include "../core/StringReader.hpp" + virtual uint16 GetId() const abstract; + virtual uint32 GetCount() const abstract; -class LanguagePack final { -public: - static LanguagePack * FromFile(uint16 id, const utf8 * path); - static LanguagePack * FromText(uint16 id, const utf8 * text); - - ~LanguagePack(); - - uint16 GetId() const { return _id; } - uint32 GetCount() const { return (uint32)_strings.size(); } - - const utf8 * GetString(rct_string_id stringId) const; - - void SetString(rct_string_id stringId, const utf8 * str) - { - if (_strings.size() >= (size_t)stringId) - { - _strings[stringId] = str; - } - } - - rct_string_id GetObjectOverrideStringId(const char * objectIdentifier, uint8 index); - rct_string_id GetScenarioOverrideStringId(const utf8 * scenarioFilename, uint8 index); - -private: - struct ObjectOverride - { - char name[8]; - const utf8 * strings[4]; - }; - - struct ScenarioOverride - { - std::string filename; - union { - const utf8 * strings[3]; - struct { - const utf8 * name; - const utf8 * park; - const utf8 * details; - }; - }; - }; - - uint16 _id; - utf8 * _stringData; - - std::vector _strings; - std::vector _objectOverrides; - std::vector _scenarioOverrides; - - LanguagePack(uint16 id, const utf8 * text); - - ObjectOverride * GetObjectOverride(const char * objectIdentifier); - ScenarioOverride * GetScenarioOverride(const utf8 * scenarioFilename); - - /////////////////////////////////////////////////////////////////////////// - // Parsing - /////////////////////////////////////////////////////////////////////////// - StringBuilder _stringDataSB; - utf8 * _currentGroup; - ObjectOverride * _currentObjectOverride; - ScenarioOverride * _currentScenarioOverride; - - void ParseLine(IStringReader * reader); - void ParseGroupObject(IStringReader * reader); - void ParseGroupScenario(IStringReader * reader); - void ParseString(IStringReader * reader); - - bool ParseToken(IStringReader * reader, uint32 * token, bool * isByte); + virtual void SetString(rct_string_id stringId, const utf8 * str) abstract; + virtual const utf8 * GetString(rct_string_id stringId) const abstract; + virtual rct_string_id GetObjectOverrideStringId(const char * objectIdentifier, uint8 index) abstract; + virtual rct_string_id GetScenarioOverrideStringId(const utf8 * scenarioFilename, uint8 index) abstract; }; + +namespace LanguagePackFactory +{ + ILanguagePack * FromFile(uint16 id, const utf8 * path); + ILanguagePack * FromText(uint16 id, const utf8 * text); +} diff --git a/src/localisation/language.cpp b/src/localisation/language.cpp index 82729db939..3cecccb3e3 100644 --- a/src/localisation/language.cpp +++ b/src/localisation/language.cpp @@ -15,6 +15,7 @@ #pragma endregion #include +#include "../core/String.hpp" #include "../object/ObjectManager.h" #include "LanguagePack.h" @@ -106,8 +107,8 @@ const language_descriptor LanguagesDescriptors[LANGUAGE_COUNT] = { int gCurrentLanguage = LANGUAGE_UNDEFINED; bool gUseTrueTypeFont = false; -LanguagePack *_languageFallback = nullptr; -LanguagePack *_languageCurrent = nullptr; +ILanguagePack *_languageFallback = nullptr; +ILanguagePack *_languageCurrent = nullptr; const utf8 BlackUpArrowString[] = { (utf8)0xC2, (utf8)0x8E, (utf8)0xE2, (utf8)0x96, (utf8)0xB2, (utf8)0x00 }; const utf8 BlackDownArrowString[] = { (utf8)0xC2, (utf8)0x8E, (utf8)0xE2, (utf8)0x96, (utf8)0xBC, (utf8)0x00 }; @@ -158,11 +159,11 @@ bool language_open(int id) platform_get_openrct_data_path(dataPath); if (id != LANGUAGE_ENGLISH_UK) { sprintf(filename, languagePath, dataPath, LanguagesDescriptors[LANGUAGE_ENGLISH_UK].locale); - _languageFallback = LanguagePack::FromFile(LANGUAGE_ENGLISH_UK, filename); + _languageFallback = LanguagePackFactory::FromFile(LANGUAGE_ENGLISH_UK, filename); } sprintf(filename, languagePath, dataPath, LanguagesDescriptors[id].locale); - _languageCurrent = LanguagePack::FromFile(id, filename); + _languageCurrent = LanguagePackFactory::FromFile(id, filename); if (_languageCurrent != nullptr) { gCurrentLanguage = id; diff --git a/src/object/StringTable.cpp b/src/object/StringTable.cpp index e4579b5465..44b5cc50df 100644 --- a/src/object/StringTable.cpp +++ b/src/object/StringTable.cpp @@ -21,6 +21,11 @@ #include "Object.h" #include "StringTable.h" +extern "C" +{ + #include "../localisation/localisation.h" +} + constexpr uint8 RCT2_LANGUAGE_ID_ENGLISH_UK = 0; constexpr uint8 RCT2_LANGUAGE_ID_BLANK = 254; constexpr uint8 RCT2_LANGUAGE_ID_END = 255;