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