diff --git a/.travis.yml b/.travis.yml index 863be8d7cd..116b0b1198 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,3 +17,6 @@ cache: apt: true sudo: required + +services: + - docker diff --git a/install.sh b/install.sh index e834c27d66..93d64307f5 100755 --- a/install.sh +++ b/install.sh @@ -74,6 +74,7 @@ if [[ `uname` == "Darwin" ]]; then popd fi elif [[ `uname` == "Linux" ]]; then + sudo apt-get update if [[ -z "$TRAVIS" ]]; then sudo apt-get install -y binutils-mingw-w64-i686 gcc-mingw-w64-i686 g++-mingw-w64-i686 cmake if [[ -z "$DISABLE_G2_BUILD" ]]; then diff --git a/projects/openrct2.vcxproj b/projects/openrct2.vcxproj index c5b0a22447..cd66b434b3 100644 --- a/projects/openrct2.vcxproj +++ b/projects/openrct2.vcxproj @@ -64,7 +64,8 @@ - + + @@ -203,6 +204,14 @@ + + + + + + + + @@ -231,6 +240,7 @@ + diff --git a/projects/openrct2.vcxproj.filters b/projects/openrct2.vcxproj.filters index 9c13072575..070539ff77 100644 --- a/projects/openrct2.vcxproj.filters +++ b/projects/openrct2.vcxproj.filters @@ -53,6 +53,9 @@ {c6b9c169-ff2a-41df-9b1c-47d15763c3e2} + + {28a808eb-9017-44cc-939b-f828fd1e2e7d} + @@ -213,9 +216,6 @@ Source\Localisation - - Source\Localisation - Source\Ride @@ -534,6 +534,12 @@ Source\World + + Source\Localisation + + + Source\Localisation + @@ -773,5 +779,32 @@ Source\Interface + + Source\Core + + + Source\Core + + + Source\Core + + + Source\Core + + + Source\Localisation + + + Source\Core + + + Source\Core + + + Source\Core + + + Source\Core + \ No newline at end of file diff --git a/src/common.h b/src/common.h index b8c710cf63..4ea6488767 100644 --- a/src/common.h +++ b/src/common.h @@ -26,4 +26,10 @@ #define SafeFree(x) if ((x) != NULL) { free(x); (x) = NULL; } +#define SafeDelete(x) if ((x) != nullptr) { delete (x); (x) = nullptr; } +#define SafeDeleteArray(x) if ((x) != nullptr) { delete[] (x); (x) = nullptr; } + +#define interface struct +#define abstract = 0 + #endif \ No newline at end of file diff --git a/src/core/Exception.hpp b/src/core/Exception.hpp new file mode 100644 index 0000000000..ac3d25d2ad --- /dev/null +++ b/src/core/Exception.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include +#include "../common.h" + +class Exception : public std::exception { +public: + Exception() : std::exception() { } + Exception(const char *message) : std::exception() { + _message = message; + } + virtual ~Exception() { } + + const char *what() const throw() override { return _message; } + const char *GetMessage() const { return _message; } + +private: + const char *_message; +}; diff --git a/src/core/FileStream.hpp b/src/core/FileStream.hpp new file mode 100644 index 0000000000..c70dfc90fa --- /dev/null +++ b/src/core/FileStream.hpp @@ -0,0 +1,94 @@ +#pragma once + +#include +#include "../common.h" +#include "IStream.hpp" + +enum { + FILE_MODE_OPEN, + FILE_MODE_WRITE +}; + +/** + * A stream for reading and writing to files. Wraps an SDL_RWops, SDL2's cross platform file stream. + */ +class FileStream : public IStream { +private: + SDL_RWops *_file; + bool _canRead; + bool _canWrite; + bool _disposed; + +public: + FileStream(const utf8 *path, int fileMode) { + const char *mode; + switch (fileMode) { + case FILE_MODE_OPEN: + mode = "rb"; + _canRead = true; + _canWrite = false; + break; + case FILE_MODE_WRITE: + mode = "wb"; + _canRead = false; + _canWrite = true; + break; + default: + throw; + } + + _file = SDL_RWFromFile(path, mode); + if (_file == NULL) { + throw IOException(SDL_GetError()); + } + + _disposed = false; + } + + ~FileStream() { + Dispose(); + } + + void Dispose() override { + if (!_disposed) { + _disposed = true; + SDL_RWclose(_file); + } + } + + bool CanRead() const override { return _canRead; } + bool CanWrite() const override { return _canWrite; } + + sint64 GetLength() const override { return SDL_RWsize(_file); } + sint64 GetPosition() const override { return SDL_RWtell(_file); } + + void SetPosition(sint64 position) override { + Seek(position, STREAM_SEEK_BEGIN); + } + + void Seek(sint64 offset, int origin) override { + switch (origin) { + case STREAM_SEEK_BEGIN: + SDL_RWseek(_file, offset, RW_SEEK_SET); + break; + case STREAM_SEEK_CURRENT: + SDL_RWseek(_file, offset, RW_SEEK_CUR); + break; + case STREAM_SEEK_END: + SDL_RWseek(_file, offset, RW_SEEK_END); + break; + } + } + + void Read(void *buffer, int length) override { + if (SDL_RWread(_file, buffer, length, 1) != 1) { + throw IOException("Attempted to read past end of file."); + } + } + + void Write(const void *buffer, int length) override { + if (SDL_RWwrite(_file, buffer, length, 1) != 1) { + throw IOException("Unable to write to file."); + } + } +}; diff --git a/src/core/IDisposable.hpp b/src/core/IDisposable.hpp new file mode 100644 index 0000000000..e0cfb58726 --- /dev/null +++ b/src/core/IDisposable.hpp @@ -0,0 +1,10 @@ +#pragma once + +#include "../common.h" + +/** + * Represents an object that can be disposed. So things can explicitly close resources before the destructor kicks in. + */ +interface IDisposable { + virtual void Dispose() abstract; +}; diff --git a/src/core/IStream.hpp b/src/core/IStream.hpp new file mode 100644 index 0000000000..00911cc6aa --- /dev/null +++ b/src/core/IStream.hpp @@ -0,0 +1,75 @@ +#pragma once + +#include "../common.h" +#include "Exception.hpp" +#include "IDisposable.hpp" + +enum { + STREAM_SEEK_BEGIN, + STREAM_SEEK_CURRENT, + STREAM_SEEK_END +}; + +/** + * Represents a stream that can be read or written to. Implemented by types such as FileStream, NetworkStream or MemoryStream. + */ +interface IStream : public IDisposable { + /////////////////////////////////////////////////////////////////////////// + // Interface methods + /////////////////////////////////////////////////////////////////////////// + // virtual ~IStream() abstract; + + virtual bool CanRead() const abstract; + virtual bool CanWrite() const abstract; + + virtual sint64 GetLength() const abstract; + virtual sint64 GetPosition() const abstract; + virtual void SetPosition(sint64 position) abstract; + virtual void Seek(sint64 offset, int origin) abstract; + + virtual void Read(void *buffer, int length) abstract; + virtual void Write(const void *buffer, int length) abstract; + + /////////////////////////////////////////////////////////////////////////// + // Helper methods + /////////////////////////////////////////////////////////////////////////// + + /** + * Reads the size of the given type from the stream directly into the given address. + */ + template + void Read(T *value) { + Read(value, sizeof(T)); + } + + /** + * Writes the size of the given type to the stream directly from the given address. + */ + template + void Write(const T *value) { + Write(value, sizeof(T)); + } + + /** + * Reads the given type from the stream. Use this only for small types (e.g. sint8, sint64, double) + */ + template + T ReadValue() { + T buffer; + Read(&buffer); + return buffer; + } + + /** + * Writes the given type to the stream. Use this only for small types (e.g. sint8, sint64, double) + */ + template + void WriteValue(const T value) { + Write(&value); + } +}; + +class IOException : public Exception { +public: + IOException(const char *message) : Exception(message) { } +}; diff --git a/src/core/Math.hpp b/src/core/Math.hpp new file mode 100644 index 0000000000..939159db08 --- /dev/null +++ b/src/core/Math.hpp @@ -0,0 +1,23 @@ +#pragma once + +/** + * Common mathematical functions. + */ +namespace Math { + + template + T Min(T a, T b) { + return a < b ? a : b; + } + + template + T Max(T a, T b) { + return a > b ? a : b; + } + + template + T Clamp(T low, T x, T high) { + return Min(Max(low, x), high); + } + +} diff --git a/src/core/Memory.hpp b/src/core/Memory.hpp new file mode 100644 index 0000000000..2e4d77c99a --- /dev/null +++ b/src/core/Memory.hpp @@ -0,0 +1,64 @@ +#pragma once + +/** + * Utility methods for memory management. Typically helpers and wrappers around the C standard library. + */ +namespace Memory { + template + T *Allocate() { + return (T*)malloc(sizeof(T)); + } + + template + T *Allocate(size_t size) { + return (T*)malloc(size); + } + + template + T *AllocateArray(size_t count) { + return (T*)malloc(count * sizeof(T)); + } + + template + T *Reallocate(T *ptr, size_t size) { + if (ptr == NULL) + return (T*)malloc(size); + else + return (T*)realloc((void*)ptr, size); + } + + template + T *ReallocateArray(T *ptr, size_t count) { + if (ptr == NULL) + return (T*)malloc(count * sizeof(T)); + else + return (T*)realloc((void*)ptr, count * sizeof(T)); + } + + template + void Free(T *ptr) { + free((void*)ptr); + } + + template + T *Copy(T *dst, const T *src, size_t size) { + return (T*)memcpy((void*)dst, (const void*)src, size); + } + + template + T *CopyArray(T *dst, const T *src, size_t count) { + return (T*)memcpy((void*)dst, (const void*)src, count * sizeof(T)); + } + + template + T *Duplicate(const T *src, size_t size) { + T *result = Allocate(size); + return Copy(result, src, size); + } + + template + T *DuplicateArray(const T *src, size_t count) { + T *result = AllocateArray(count); + return CopyArray(result, src, count); + } +} diff --git a/src/core/StringBuilder.hpp b/src/core/StringBuilder.hpp new file mode 100644 index 0000000000..b96c1cbe89 --- /dev/null +++ b/src/core/StringBuilder.hpp @@ -0,0 +1,116 @@ +#pragma once + +#include "../common.h" +#include "../localisation/localisation.h" +#include "Math.hpp" +#include "Memory.hpp" + +/** + * Class for constructing strings efficiently. A buffer is automatically allocated and reallocated when characters or strings + * are appended. Use GetString to copy the current state of the string builder to a new fire-and-forget string. + */ +class StringBuilder final { +public: + StringBuilder() { + _buffer = NULL; + _capacity = 0; + _length = 0; + } + + StringBuilder(int capacity) : StringBuilder() { + EnsureCapacity(capacity); + } + + ~StringBuilder() { + if (_buffer != NULL) Memory::Free(_buffer); + } + + /** + * Appends the given character to the current string. + */ + void Append(int codepoint) { + int codepointLength = utf8_get_codepoint_length(codepoint); + EnsureCapacity(_length + codepointLength + 1); + utf8_write_codepoint(_buffer + _length, codepoint); + _length += codepointLength; + _buffer[_length] = 0; + } + + /** + * Appends the given string to the current string. + */ + void Append(const utf8 *text) { + int textLength = strlen(text); + + EnsureCapacity(_length + textLength + 1); + Memory::Copy(_buffer + _length, text, textLength); + _length += textLength; + _buffer[_length] = 0; + } + + /** + * Clears the current string, but preserves the allocated memory for another string. + */ + void Clear() { + _length = 0; + if (_capacity >= 1) { + _buffer[_length] = 0; + } + } + + /** + * Like Clear, only will guarantee freeing of the underlying buffer. + */ + void Reset() { + _length = 0; + _capacity = 0; + if (_buffer != NULL) { + Memory::Free(_buffer); + } + } + + /** + * Returns the current string buffer as a new fire-and-forget string. + */ + utf8 *GetString() const { + utf8 *result = Memory::AllocateArray(_length + 1); + Memory::CopyArray(result, _buffer, _length); + result[_length] = 0; + return result; + } + + /** + * Gets the current state of the StringBuilder. Warning: this represents the StringBuilder's current working buffer and will + * be deallocated when the StringBuilder is destructed. + */ + const utf8 *GetBuffer() const { + return _buffer; + } + + /** + * Gets the amount of allocated memory for the string buffer. + */ + size_t GetCapacity() const { return _capacity; } + + /** + * Gets the length of the current string. + */ + size_t GetLength() const { return _length; } + +private: + utf8 *_buffer; + size_t _capacity; + size_t _length; + + void EnsureCapacity(size_t capacity) + { + if (_capacity > capacity) return; + + _capacity = Math::Max(8U, _capacity); + while (_capacity < capacity) { + _capacity *= 2; + } + + _buffer = Memory::ReallocateArray(_buffer, _capacity); + } +}; diff --git a/src/core/StringReader.hpp b/src/core/StringReader.hpp new file mode 100644 index 0000000000..196470cc87 --- /dev/null +++ b/src/core/StringReader.hpp @@ -0,0 +1,58 @@ +#pragma once + +#include "../common.h" +#include "../localisation/localisation.h" +#include "../util/util.h" + +interface IStringReader { + virtual bool TryPeek(int *outCodepoint) abstract; + virtual bool TryRead(int *outCodepoint) abstract; + virtual void Skip() abstract; + virtual bool CanRead() const abstract; +}; + +class UTF8StringReader final : public IStringReader { +public: + UTF8StringReader(const utf8 *text) { + // Skip UTF-8 byte order mark + if (strlen(text) >= 3 && utf8_is_bom(text)) { + text += 3; + } + + _text = text; + _current = text; + } + + bool TryPeek(int *outCodepoint) override { + if (_current == NULL) return false; + + int codepoint = utf8_get_next(_current, NULL); + *outCodepoint = codepoint; + return true; + } + + bool TryRead(int *outCodepoint) override { + if (_current == NULL) return false; + + int codepoint = utf8_get_next(_current, &_current); + *outCodepoint = codepoint; + if (codepoint == 0) { + _current = NULL; + return false; + } + return true; + } + + void Skip() override { + int codepoint; + TryRead(&codepoint); + } + + bool CanRead() const override { + return _current != NULL; + } + +private: + const utf8 *_text; + const utf8 *_current; +}; diff --git a/src/localisation/LanguagePack.cpp b/src/localisation/LanguagePack.cpp new file mode 100644 index 0000000000..cacc01da88 --- /dev/null +++ b/src/localisation/LanguagePack.cpp @@ -0,0 +1,473 @@ +extern "C" { + #include "../common.h" + #include "../util/util.h" + #include "localisation.h" +} + +#include "../core/FileStream.hpp" +#include "../core/Memory.hpp" +#include "../core/StringBuilder.hpp" +#include "LanguagePack.h" +#include + +constexpr rct_string_id ObjectOverrideBase = 0x6000; +constexpr int ObjectOverrideMaxStringCount = 4; + +constexpr rct_string_id ScenarioOverrideBase = 0x7000; +constexpr int ScenarioOverrideMaxStringCount = 3; + +LanguagePack *LanguagePack::FromFile(int id, const utf8 *path) +{ + assert(path != NULL); + + uint32 fileLength; + utf8 *fileData; + + // Load file directly into memory + try { + FileStream fs = FileStream(path, FILE_MODE_OPEN); + + fileLength = (uint32)fs.GetLength(); + fileData = Memory::Allocate(fileLength); + fs.Read(fileData, fileLength); + + fs.Dispose(); + } catch (Exception ex) { + log_error("Unable to open %s: %s", path, ex.GetMessage()); + return NULL; + } + + // Parse the memory as text + LanguagePack *result = FromText(id, fileData); + + Memory::Free(fileData); + return result; +} + +LanguagePack *LanguagePack::FromText(int id, const utf8 *text) +{ + return new LanguagePack(id, text); +} + +LanguagePack::LanguagePack(int id, const utf8 *text) +{ + assert(text != NULL); + + _id = id; + _stringData = NULL; + _currentGroup = NULL; + _currentObjectOverride = NULL; + _currentScenarioOverride = NULL; + + auto reader = UTF8StringReader(text); + while (reader.CanRead()) { + ParseLine(&reader); + } + + _stringData = _stringDataSB.GetString(); + + size_t stringDataBaseAddress = (size_t)_stringData; + for (size_t i = 0; i < _strings.size(); i++) { + _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 != NULL) { + *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 != NULL) { + *strPtr = (utf8*)(stringDataBaseAddress + (size_t)*strPtr); + } + } + } + + // Destruct the string builder to free memory + _stringDataSB = StringBuilder(); +} + +LanguagePack::~LanguagePack() +{ + SafeFree(_stringData); + SafeFree(_currentGroup); +} + +const utf8 *LanguagePack::GetString(int 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 NULL; + } + }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 NULL; + } + } else { + if (_strings.size() > (size_t)stringId) { + return _strings[stringId]; + } else { + return NULL; + } + } +} + +rct_string_id LanguagePack::GetObjectOverrideStringId(const char *objectIdentifier, int index) +{ + assert(objectIdentifier != NULL); + assert(index < ObjectOverrideMaxStringCount); + + int ooIndex = 0; + for (const ObjectOverride &objectOverride : _objectOverrides) { + if (strncmp(objectOverride.name, objectIdentifier, 8) == 0) { + if (objectOverride.strings[index] == NULL) { + return STR_NONE; + } + return ObjectOverrideBase + (ooIndex * ObjectOverrideMaxStringCount) + index; + } + ooIndex++; + } + + return STR_NONE; +} + +rct_string_id LanguagePack::GetScenarioOverrideStringId(const utf8 *scenarioFilename, int index) +{ + assert(scenarioFilename != NULL); + assert(index < ScenarioOverrideMaxStringCount); + + int ooIndex = 0; + for (const ScenarioOverride &scenarioOverride : _scenarioOverrides) { + if (_stricmp(scenarioOverride.filename, scenarioFilename) == 0) { + if (scenarioOverride.strings[index] == NULL) { + return STR_NONE; + } + return ScenarioOverrideBase + (ooIndex * ScenarioOverrideMaxStringCount) + index; + } + ooIndex++; + } + + return STR_NONE; +} + +LanguagePack::ObjectOverride *LanguagePack::GetObjectOverride(const char *objectIdentifier) +{ + assert(objectIdentifier != NULL); + + for (size_t i = 0; i < _objectOverrides.size(); i++) { + ObjectOverride *oo = &_objectOverrides[i]; + if (strncmp(oo->name, objectIdentifier, 8) == 0) { + return oo; + } + } + return false; +} + +LanguagePack::ScenarioOverride *LanguagePack::GetScenarioOverride(const utf8 *scenarioIdentifier) +{ + assert(scenarioIdentifier != NULL); + + for (size_t i = 0; i < _scenarioOverrides.size(); i++) { + ScenarioOverride *so = &_scenarioOverrides[i]; + if (_stricmp(so->name, scenarioIdentifier) == 0) { + return so; + } + } + return false; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// 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(int codepoint) +{ + return codepoint == '\t' || codepoint == ' ' || codepoint == '\r' || codepoint == '\n'; +} + +static bool IsNewLine(int codepoint) +{ + return codepoint == '\r' || codepoint == '\n'; +} + +static void SkipWhitespace(IStringReader *reader) +{ + int codepoint; + while (reader->TryPeek(&codepoint)) { + if (IsWhitespace(codepoint)) { + reader->Skip(); + } else { + break; + } + } +} + +static void SkipNewLine(IStringReader *reader) +{ + int codepoint; + while (reader->TryPeek(&codepoint)) { + if (IsNewLine(codepoint)) { + reader->Skip(); + } else { + break; + } + } +} + +static void SkipToEndOfLine(IStringReader *reader) +{ + int codepoint; + while (reader->TryPeek(&codepoint)) { + if (codepoint != '\r' && codepoint != '\n') { + reader->Skip(); + } else { + break; + } + } +} + +void LanguagePack::ParseLine(IStringReader *reader) +{ + SkipWhitespace(reader); + + int 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(); + int 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 = NULL; + if (_currentObjectOverride == NULL) { + _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(); + int 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 = NULL; + _currentScenarioOverride = GetScenarioOverride(_currentGroup); + if (_currentScenarioOverride == NULL) { + _scenarioOverrides.push_back(ScenarioOverride()); + _currentScenarioOverride = &_scenarioOverrides[_scenarioOverrides.size() - 1]; + memset(_currentScenarioOverride, 0, sizeof(ObjectOverride)); + _currentScenarioOverride->filename = sb.GetString(); + } + } +} + +void LanguagePack::ParseString(IStringReader *reader) +{ + auto sb = StringBuilder(); + int 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 == NULL) { + if (sscanf(identifier, "STR_%4d", &stringId) != 1) { + // Ignore line entirely + return; + } + } else { + if (strcmp(identifier, "STR_NAME") == 0) { stringId = 0; } + else if (strcmp(identifier, "STR_DESC") == 0) { stringId = 1; } + else if (strcmp(identifier, "STR_CPTY") == 0) { stringId = 2; } + + else if (strcmp(identifier, "STR_SCNR") == 0) { stringId = 0; } + else if (strcmp(identifier, "STR_PARK") == 0) { stringId = 1; } + else if (strcmp(identifier, "STR_DTLS") == 0) { 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; + if (!ParseToken(reader, &token)) { + // Syntax error or unknown token, ignore line entirely + return; + } else { + sb.Append((int)token); + } + } 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 == NULL) { + // Make sure the list is big enough to contain this string id + while (_strings.size() <= (size_t)stringId) { + _strings.push_back(NULL); + } + + _strings[stringId] = relativeOffset; + } else { + if (_currentObjectOverride != NULL) { + _currentObjectOverride->strings[stringId] = relativeOffset; + } else { + _currentScenarioOverride->strings[stringId] = relativeOffset; + } + } + + _stringDataSB.Append(sb.GetBuffer()); +} + +bool LanguagePack::ParseToken(IStringReader *reader, uint32 *token) +{ + auto sb = StringBuilder(); + int codepoint; + + // Skip open brace + reader->Skip(); + + while (reader->TryPeek(&codepoint)) { + 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); + return true; +} diff --git a/src/localisation/LanguagePack.h b/src/localisation/LanguagePack.h new file mode 100644 index 0000000000..56a3479184 --- /dev/null +++ b/src/localisation/LanguagePack.h @@ -0,0 +1,77 @@ +#pragma once + +#include + +extern "C" { + #include "../common.h" + #include "../util/util.h" + #include "localisation.h" +} + +#include "../core/StringBuilder.hpp" +#include "../core/StringReader.hpp" + +class LanguagePack final { +public: + static LanguagePack *FromFile(int id, const utf8 *path); + static LanguagePack *FromText(int id, const utf8 *text); + + ~LanguagePack(); + + int GetId() const { return _id; } + int GetCount() const { return _strings.size(); } + + const utf8 *GetString(int stringId) const; + + void SetString(int stringId, const utf8 *str) { + if (_strings.size() >= (size_t)stringId) { + _strings[stringId] = str; + } + } + + rct_string_id GetObjectOverrideStringId(const char *objectIdentifier, int index); + rct_string_id GetScenarioOverrideStringId(const utf8 *scenarioFilename, int index); + +private: + struct ObjectOverride { + char name[8]; + const utf8 *strings[4]; + }; + + struct ScenarioOverride { + const utf8 *filename; + union { + const utf8 *strings[3]; + struct { + const utf8 *name; + const utf8 *park; + const utf8 *details; + }; + }; + }; + + int _id; + utf8 *_stringData; + std::vector _strings; + std::vector _objectOverrides; + std::vector _scenarioOverrides; + + LanguagePack(int 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); +}; diff --git a/src/localisation/language.c b/src/localisation/language.cpp similarity index 66% rename from src/localisation/language.c rename to src/localisation/language.cpp index 089f4f25b9..5d3323408a 100644 --- a/src/localisation/language.c +++ b/src/localisation/language.cpp @@ -18,6 +18,10 @@ * along with this program. If not, see . *****************************************************************************/ +#include "LanguagePack.h" + +extern "C" { + #include "../addresses.h" #include "../drawing/drawing.h" #include "../object.h" @@ -25,14 +29,6 @@ #include "../util/util.h" #include "localisation.h" -typedef struct { - int id; - int num_strings; - char **strings; - size_t string_data_size; - char *string_data; -} language_data; - enum { RCT2_LANGUAGE_ID_ENGLISH_UK, RCT2_LANGUAGE_ID_ENGLISH_US, @@ -88,25 +84,22 @@ const language_descriptor LanguagesDescriptors[LANGUAGE_COUNT] = { { "zh-Hant", "Chinese (Traditional)", "Chinese (Traditional)", "chinese_traditional", &TTFFontMingLiu, RCT2_LANGUAGE_ID_CHINESE_TRADITIONAL }, // LANGUAGE_CHINESE_TRADITIONAL { "zh-Hans", "Chinese (Simplified)", "Chinese (Simplified)", "chinese_simplified", &TTFFontSimSun, RCT2_LANGUAGE_ID_CHINESE_SIMPLIFIED }, // LANGUAGE_CHINESE_SIMPLIFIED { "fi-FI", "Finnish", "Suomi", "finnish", FONT_OPENRCT2_SPRITE, RCT2_LANGUAGE_ID_ENGLISH_UK }, // LANGUAGE_FINNISH - { "kr-KR", "Korean", "Korean", "korean", &TTFFontMalgun, RCT2_LANGUAGE_ID_ENGLISH_UK }, // LANGUAGE_KOREAN + { "kr-KR", "Korean", "Korean", "korean", &TTFFontMalgun, RCT2_LANGUAGE_ID_KOREAN }, // LANGUAGE_KOREAN }; int gCurrentLanguage = LANGUAGE_UNDEFINED; bool gUseTrueTypeFont = false; -language_data _languageFallback = { 0 }; -language_data _languageCurrent = { 0 }; +LanguagePack *_languageFallback = nullptr; +LanguagePack *_languageCurrent = nullptr; -const char **_languageOriginal = (char**)0x009BF2D4; +const char **_languageOriginal = (const char**)0x009BF2D4; -const utf8 BlackUpArrowString[] = { 0xC2, 0x8E, 0xE2, 0x96, 0xB2, 0x00 }; -const utf8 BlackDownArrowString[] = { 0xC2, 0x8E, 0xE2, 0x96, 0xBC, 0x00 }; -const utf8 BlackLeftArrowString[] = { 0xC2, 0x8E, 0xE2, 0x97, 0x80, 0x00 }; -const utf8 BlackRightArrowString[] = { 0xC2, 0x8E, 0xE2, 0x96, 0xB6, 0x00 }; -const utf8 CheckBoxMarkString[] = { 0xE2, 0x9C, 0x93, 0x00 }; - -static int language_open_file(const utf8 *filename, language_data *language); -static void language_close(language_data *language); +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 }; +const utf8 BlackLeftArrowString[] = { (utf8)0xC2, (utf8)0x8E, (utf8)0xE2, (utf8)0x97, (utf8)0x80, (utf8)0x00 }; +const utf8 BlackRightArrowString[] = { (utf8)0xC2, (utf8)0x8E, (utf8)0xE2, (utf8)0x96, (utf8)0xB6, (utf8)0x00 }; +const utf8 CheckBoxMarkString[] = { (utf8)0xE2, (utf8)0x9C, (utf8)0x93, (utf8)0x00 }; void utf8_remove_format_codes(utf8 *text) { @@ -128,10 +121,10 @@ const char *language_get_string(rct_string_id id) if (id == (rct_string_id)STR_NONE) return NULL; - if (_languageCurrent.num_strings > id) - openrctString = _languageCurrent.strings[id]; - if (openrctString == NULL && _languageFallback.num_strings > id) - openrctString = _languageFallback.strings[id]; + if (_languageCurrent != nullptr) + openrctString = _languageCurrent->GetString(id); + if (openrctString == NULL && _languageFallback != nullptr) + openrctString = _languageFallback->GetString(id); if (id >= STR_OPENRCT2_BEGIN_STRING_ID) { return openrctString != NULL ? openrctString : "(undefined string)"; @@ -153,14 +146,12 @@ int language_open(int id) if (id != LANGUAGE_ENGLISH_UK) { sprintf(filename, languagePath, gExePath, LanguagesDescriptors[LANGUAGE_ENGLISH_UK].path); - if (language_open_file(filename, &_languageFallback)) { - _languageFallback.id = LANGUAGE_ENGLISH_UK; - } + _languageFallback = LanguagePack::FromFile(LANGUAGE_ENGLISH_UK, filename); } sprintf(filename, languagePath, gExePath, LanguagesDescriptors[id].path); - if (language_open_file(filename, &_languageCurrent)) { - _languageCurrent.id = id; + _languageCurrent = LanguagePack::FromFile(id, filename); + if (_languageCurrent != NULL) { gCurrentLanguage = id; if (LanguagesDescriptors[id].font == FONT_OPENRCT2_SPRITE) { @@ -184,148 +175,11 @@ int language_open(int id) void language_close_all() { - language_close(&_languageFallback); - language_close(&_languageCurrent); - _languageFallback.id = LANGUAGE_UNDEFINED; - _languageCurrent.id = LANGUAGE_UNDEFINED; + SafeDelete(_languageFallback); + SafeDelete(_languageCurrent); gCurrentLanguage = LANGUAGE_UNDEFINED; } -/** - * 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. - * - * Also note that all strings are currently still ASCII. It probably can't be converted to UTF-8 until all game functions that - * read / write strings in some way is decompiled. The original game used a DIY extended 8-bit extended ASCII set for special - * characters, format codes and accents. - * - * In terms of 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 int language_open_file(const utf8 *filename, language_data *language) -{ - assert(filename != NULL); - assert(language != NULL); - - SDL_RWops *f = SDL_RWFromFile(filename, "rb"); - if (f == NULL) - return 0; - - SDL_RWseek(f, 0, RW_SEEK_END); - language->string_data_size = (size_t)(SDL_RWtell(f) + 1); - language->string_data = calloc(1, language->string_data_size); - SDL_RWseek(f, 0, RW_SEEK_SET); - SDL_RWread(f, language->string_data, language->string_data_size, 1); - SDL_RWclose(f); - - language->strings = calloc(STR_COUNT, sizeof(char*)); - - char *dst = NULL; - char *token = NULL; - char tokenBuffer[64]; - int stringIndex = 0, mode = 0, stringId, maxStringId = 0; - size_t i = 0; - - // Skim UTF-8 byte order mark - if (utf8_is_bom(language->string_data)) - i += 3; - - for (; i < language->string_data_size; i++) { - char *src = &language->string_data[i]; - - // Handle UTF-8 - char *srcNext; - uint32 utf8Char = utf8_get_next(src, (const utf8**)&srcNext); - i += srcNext - src - 1; - - switch (mode) { - case 0: - // Search for a comment - if (utf8Char == '#') { - mode = 3; - } else if (utf8Char == ':' && stringId != -1) { - // Search for colon - dst = src + 1; - language->strings[stringId] = dst; - stringIndex++; - mode = 1; - } else if (!strncmp(src, "STR_", 4)){ - // Copy in the string number, 4 characters only - if (sscanf(src, "STR_%4d", &stringId) != 1) { - stringId = -1; - } else { - maxStringId = max(maxStringId, stringId); - } - } - break; - case 1: - // Copy string over, stop at line break - if (utf8Char == '{') { - token = src + 1; - mode = 2; - } else if (utf8Char == '\n' || *src == '\r') { - *dst = 0; - mode = 0; - } else { - dst = utf8_write_codepoint(dst, utf8Char); - } - break; - case 2: - // Read token, convert to code - if (utf8Char == '}') { - int tokenLength = min(src - token, sizeof(tokenBuffer) - 1); - memcpy(tokenBuffer, token, tokenLength); - tokenBuffer[tokenLength] = 0; - uint32 code = format_get_code(tokenBuffer); - if (code == 0) { - code = atoi(tokenBuffer); - *dst++ = code & 0xFF; - } else { - dst = utf8_write_codepoint(dst, code); - } - mode = 1; - } - break; - case 3: - if (utf8Char == '\n' || utf8Char == '\r') { - mode = 0; - } - } - } - language->num_strings = maxStringId + 1; - language->strings = realloc(language->strings, language->num_strings * sizeof(char*)); - - return 1; -} - -static void language_close(language_data *language) -{ - SafeFree(language->strings); - SafeFree(language->string_data); - language->num_strings = 0; - language->string_data_size = 0; -} - -const int OpenRCT2LangIdToObjectLangId[] = { - 0, - 0, - 1, - 3, - 6, - 2, - 0, - 0, - 4, - 7, - 5, - 13 -}; - #define STEX_BASE_STRING_ID 3447 #define NONSTEX_BASE_STRING_ID 3463 #define MAX_OBJECT_CACHED_STRINGS 2048 @@ -379,9 +233,9 @@ static wchar_t convert_specific_language_character_to_unicode(int languageId, wc static utf8 *convert_multibyte_charset(const char *src, int languageId) { int reservedLength = (strlen(src) * 4) + 1; - utf8 *buffer = malloc(reservedLength); + utf8 *buffer = (utf8*)malloc(reservedLength); utf8 *dst = buffer; - for (const uint8 *ch = src; *ch != 0;) { + for (const uint8 *ch = (const uint8*)src; *ch != 0;) { if (*ch == 0xFF) { ch++; uint8 a = *ch++; @@ -396,7 +250,7 @@ static utf8 *convert_multibyte_charset(const char *src, int languageId) } *dst++ = 0; int actualLength = dst - buffer; - buffer = realloc(buffer, actualLength); + buffer = (utf8*)realloc(buffer, actualLength); return buffer; } @@ -420,14 +274,14 @@ rct_string_id object_get_localised_text(uint8_t** pStringTable/*ebp*/, int type/ char *pString = NULL; int result = 0; bool isBlank; - + while ((languageId = *(*pStringTable)++) != RCT2_LANGUAGE_ID_END) { isBlank = true; // Strings that are just ' ' are set as invalid langauges. // But if there is no real string then it will set the string as // the blank string - for (char *ch = *pStringTable; *ch != 0; ch++) { + for (char *ch = (char*)(*pStringTable); *ch != 0; ch++) { if (!isblank(*ch)) { isBlank = false; break; @@ -439,21 +293,21 @@ rct_string_id object_get_localised_text(uint8_t** pStringTable/*ebp*/, int type/ // This is the ideal situation. Language found if (languageId == LanguagesDescriptors[gCurrentLanguage].rct2_original_id) { chosenLanguageId = languageId; - pString = *pStringTable; + pString = (char*)(*pStringTable); result |= 1; } // Just in case always load english into pString if (languageId == RCT2_LANGUAGE_ID_ENGLISH_UK && !(result & 1)) { chosenLanguageId = languageId; - pString = *pStringTable; + pString = (char*)(*pStringTable); result |= 2; } // Failing that fall back to whatever is first string if (!(result & 7)) { chosenLanguageId = languageId; - pString = *pStringTable; + pString = (char*)(*pStringTable); if (!isBlank) result |= 4; } @@ -461,6 +315,19 @@ rct_string_id object_get_localised_text(uint8_t** pStringTable/*ebp*/, int type/ while (*(*pStringTable)++ != 0); } + char name[9]; + if (RCT2_GLOBAL(0x009ADAFC, uint8) == 0) { + memcpy(name, object_entry_groups[type].entries[index].name, 8); + } else { + memcpy(name, gTempObjectLoadName, 8); + } + name[8] = 0; + + rct_string_id stringId = _languageCurrent->GetObjectOverrideStringId(name, tableindex); + if (stringId != (rct_string_id)STR_NONE) { + return stringId; + } + // If not scenario text if (RCT2_GLOBAL(0x009ADAFC, uint8) == 0) { int stringid = NONSTEX_BASE_STRING_ID; @@ -488,8 +355,7 @@ rct_string_id object_get_localised_text(uint8_t** pStringTable/*ebp*/, int type/ utf8_trim_string(*cacheString); //put pointer in stringtable - if (_languageCurrent.num_strings > stringid) - _languageCurrent.strings[stringid] = *cacheString; + _languageCurrent->SetString(stringid, *cacheString); // Until all string related functions are finished copy // to old array as well. _languageOriginal[stringid] = *cacheString; @@ -511,11 +377,23 @@ rct_string_id object_get_localised_text(uint8_t** pStringTable/*ebp*/, int type/ utf8_trim_string(*cacheString); //put pointer in stringtable - if (_languageCurrent.num_strings > stringid) - _languageCurrent.strings[stringid] = *cacheString; + _languageCurrent->SetString(stringid, *cacheString); // Until all string related functions are finished copy // to old array as well. _languageOriginal[stringid] = *cacheString; return stringid; } } + +bool language_get_localised_scenario_strings(const utf8 *scenarioFilename, rct_string_id *outStringIds) +{ + outStringIds[0] = _languageCurrent->GetScenarioOverrideStringId(scenarioFilename, 0); + outStringIds[1] = _languageCurrent->GetScenarioOverrideStringId(scenarioFilename, 1); + outStringIds[2] = _languageCurrent->GetScenarioOverrideStringId(scenarioFilename, 2); + return + outStringIds[0] != (rct_string_id)STR_NONE || + outStringIds[1] != (rct_string_id)STR_NONE || + outStringIds[2] != (rct_string_id)STR_NONE; +} + +} diff --git a/src/localisation/language.h b/src/localisation/language.h index 66b7928306..4cc301146e 100644 --- a/src/localisation/language.h +++ b/src/localisation/language.h @@ -82,4 +82,6 @@ int utf8_length(const utf8 *text); wchar_t *utf8_to_widechar(const utf8 *src); utf8 *widechar_to_utf8(const wchar_t *src); +bool language_get_localised_scenario_strings(const utf8 *scenarioFilename, rct_string_id *outStringIds); + #endif diff --git a/src/object.c b/src/object.c index c5d7af1f37..4295cbb146 100644 --- a/src/object.c +++ b/src/object.c @@ -33,6 +33,8 @@ #include "scenario.h" #include "rct1.h" +char gTempObjectLoadName[9] = { 0 }; + int object_load_entry(const utf8 *path, rct_object_entry *outEntry) { SDL_RWops *file; @@ -1566,6 +1568,7 @@ int object_get_scenario_text(rct_object_entry *entry) // Tell text to be loaded into a different address RCT2_GLOBAL(0x009ADAFC, uint8) = 255; + memcpy(gTempObjectLoadName, openedEntry.name, 8); // Not used?? RCT2_GLOBAL(0x009ADAFD, uint8) = 1; object_paint(openedEntry.flags & 0x0F, 0, 0, 0, 0, (int)chunk, 0, 0); diff --git a/src/object.h b/src/object.h index ad7d7c66bd..18c11087d9 100644 --- a/src/object.h +++ b/src/object.h @@ -91,6 +91,7 @@ typedef struct { } rct_object_filters; extern rct_object_entry_group object_entry_groups[]; +extern char gTempObjectLoadName[9]; int object_load_entry(const utf8 *path, rct_object_entry *outEntry); void object_list_load(); diff --git a/src/object_list.c b/src/object_list.c index 1e96973ee5..7fd147949c 100644 --- a/src/object_list.c +++ b/src/object_list.c @@ -27,6 +27,10 @@ #include "util/sawyercoding.h" #include "game.h" #include "rct1.h" +#include "world/entrance.h" +#include "world/footpath.h" +#include "world/scenery.h" +#include "world/water.h" #define OBJECT_ENTRY_GROUP_COUNT 11 #define OBJECT_ENTRY_COUNT 721 @@ -673,6 +677,33 @@ rct_object_entry *object_list_find(rct_object_entry *entry) return NULL; } +rct_string_id object_get_name_string_id(rct_object_entry *entry, const void *chunk) +{ + int objectType = entry->flags & 0x0F; + switch (objectType) { + case OBJECT_TYPE_RIDE: + return ((rct_ride_type*)chunk)->name; + case OBJECT_TYPE_SMALL_SCENERY: + case OBJECT_TYPE_LARGE_SCENERY: + case OBJECT_TYPE_WALLS: + case OBJECT_TYPE_BANNERS: + case OBJECT_TYPE_PATH_BITS: + return ((rct_scenery_entry*)chunk)->name; + case OBJECT_TYPE_PATHS: + return ((rct_path_type*)chunk)->string_idx; + case OBJECT_TYPE_SCENERY_SETS: + return ((rct_scenery_set_entry*)chunk)->name; + case OBJECT_TYPE_PARK_ENTRANCE: + return ((rct_entrance_type*)chunk)->string_idx; + case OBJECT_TYPE_WATER: + return ((rct_water_type*)chunk)->string_idx; + case OBJECT_TYPE_SCENARIO_TEXT: + return ((rct_stex_entry*)chunk)->scenario_name; + default: + return STR_NONE; + } +} + /* Installs an object_entry at the desired installed_entry address * Returns the size of the new entry. Will return 0 on failure. */ @@ -740,7 +771,12 @@ static uint32 install_object_entry(rct_object_entry* entry, rct_object_entry* in load_object_filter(entry, chunk, filter); // Always extract only the vehicle type, since the track type is always displayed in the left column, to prevent duplicate track names. - strcpy(installed_entry_pointer, language_get_string((rct_string_id)RCT2_GLOBAL(RCT2_ADDRESS_CURR_OBJECT_BASE_STRING_ID, uint32))); + rct_string_id nameStringId = object_get_name_string_id(entry, chunk); + if (nameStringId == STR_NONE) { + nameStringId = (rct_string_id)RCT2_GLOBAL(RCT2_ADDRESS_CURR_OBJECT_BASE_STRING_ID, uint32); + } + + strcpy(installed_entry_pointer, language_get_string(nameStringId)); while (*installed_entry_pointer++); // This is deceptive. Due to setting the total no images earlier to 0xF26E diff --git a/src/scenario.c b/src/scenario.c index 0a8ddebea0..d8260bae5f 100644 --- a/src/scenario.c +++ b/src/scenario.c @@ -73,14 +73,29 @@ int scenario_load_basic(const char *path, rct_s6_header *header, rct_s6_info *in SDL_RWclose(rw); RCT2_GLOBAL(0x009AA00C, uint8) = 0; - // Checks for a scenario string object (possibly for localisation) - if ((info->entry.flags & 0xFF) != 255) { - if (object_get_scenario_text(&info->entry)) { - rct_stex_entry* stex_entry = RCT2_GLOBAL(RCT2_ADDRESS_SCENARIO_TEXT_TEMP_CHUNK, rct_stex_entry*); - format_string(info->name, stex_entry->scenario_name, NULL); - format_string(info->details, stex_entry->details, NULL); - RCT2_GLOBAL(0x009AA00C, uint8) = stex_entry->var_06; - object_free_scenario_text(); + // Get filename + utf8 filename[MAX_PATH]; + strcpy(filename, path_get_filename(path)); + path_remove_extension(filename); + + rct_string_id localisedStringIds[3]; + if (language_get_localised_scenario_strings(filename, localisedStringIds)) { + if (localisedStringIds[0] != (rct_string_id)STR_NONE) { + strncpy(info->name, language_get_string(localisedStringIds[0]), 64); + } + if (localisedStringIds[2] != (rct_string_id)STR_NONE) { + strncpy(info->details, language_get_string(localisedStringIds[2]), 256); + } + } else { + // Checks for a scenario string object (possibly for localisation) + if ((info->entry.flags & 0xFF) != 255) { + if (object_get_scenario_text(&info->entry)) { + rct_stex_entry* stex_entry = RCT2_GLOBAL(RCT2_ADDRESS_SCENARIO_TEXT_TEMP_CHUNK, rct_stex_entry*); + format_string(info->name, stex_entry->scenario_name, NULL); + format_string(info->details, stex_entry->details, NULL); + RCT2_GLOBAL(0x009AA00C, uint8) = stex_entry->var_06; + object_free_scenario_text(); + } } } return 1; @@ -282,23 +297,45 @@ void scenario_begin() strcpy((char*)RCT2_ADDRESS_SCENARIO_DETAILS, s6Info->details); strcpy((char*)RCT2_ADDRESS_SCENARIO_NAME, s6Info->name); - rct_stex_entry* stex = g_stexEntries[0]; - if ((int)stex != -1) { - char *buffer = (char*)RCT2_ADDRESS_COMMON_STRING_FORMAT_BUFFER; + { + // Get filename + utf8 filename[MAX_PATH]; + strcpy(filename, _scenarioFileName); + path_remove_extension(filename); - // Set localised park name - format_string(buffer, stex->park_name, 0); - park_set_name(buffer); + rct_string_id localisedStringIds[3]; + if (language_get_localised_scenario_strings(filename, localisedStringIds)) { + if (localisedStringIds[0] != (rct_string_id)STR_NONE) { + strncpy((char*)RCT2_ADDRESS_SCENARIO_NAME, language_get_string(localisedStringIds[0]), 31); + ((char*)RCT2_ADDRESS_SCENARIO_NAME)[31] = '\0'; + } + if (localisedStringIds[1] != (rct_string_id)STR_NONE) { + park_set_name(language_get_string(localisedStringIds[1])); + } + if (localisedStringIds[2] != (rct_string_id)STR_NONE) { + strncpy((char*)RCT2_ADDRESS_SCENARIO_DETAILS, language_get_string(localisedStringIds[2]), 255); + ((char*)RCT2_ADDRESS_SCENARIO_DETAILS)[255] = '\0'; + } + } else { + rct_stex_entry* stex = g_stexEntries[0]; + if ((int)stex != -1) { + char *buffer = (char*)RCT2_ADDRESS_COMMON_STRING_FORMAT_BUFFER; - // Set localised scenario name - format_string(buffer, stex->scenario_name, 0); - strncpy((char*)RCT2_ADDRESS_SCENARIO_NAME, buffer, 31); - ((char*)RCT2_ADDRESS_SCENARIO_NAME)[31] = '\0'; + // Set localised park name + format_string(buffer, stex->park_name, 0); + park_set_name(buffer); - // Set localised scenario details - format_string(buffer, stex->details, 0); - strncpy((char*)RCT2_ADDRESS_SCENARIO_DETAILS, buffer, 255); - ((char*)RCT2_ADDRESS_SCENARIO_DETAILS)[255] = '\0'; + // Set localised scenario name + format_string(buffer, stex->scenario_name, 0); + strncpy((char*)RCT2_ADDRESS_SCENARIO_NAME, buffer, 31); + ((char*)RCT2_ADDRESS_SCENARIO_NAME)[31] = '\0'; + + // Set localised scenario details + format_string(buffer, stex->details, 0); + strncpy((char*)RCT2_ADDRESS_SCENARIO_DETAILS, buffer, 255); + ((char*)RCT2_ADDRESS_SCENARIO_DETAILS)[255] = '\0'; + } + } } // Set the last saved game path