mirror of
https://github.com/OpenRCT2/OpenRCT2
synced 2026-01-20 13:33:02 +01:00
Merge pull request #1907 from OpenRCT2/language-dat
Localisation for objects and scenarios.
This commit is contained in:
@@ -17,3 +17,6 @@ cache:
|
||||
apt: true
|
||||
|
||||
sudo: required
|
||||
|
||||
services:
|
||||
- docker
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -64,7 +64,8 @@
|
||||
<ClCompile Include="..\src\localisation\convert.c" />
|
||||
<ClCompile Include="..\src\localisation\currency.c" />
|
||||
<ClCompile Include="..\src\localisation\date.c" />
|
||||
<ClCompile Include="..\src\localisation\language.c" />
|
||||
<ClCompile Include="..\src\localisation\language.cpp" />
|
||||
<ClCompile Include="..\src\localisation\LanguagePack.cpp" />
|
||||
<ClCompile Include="..\src\localisation\localisation.c" />
|
||||
<ClCompile Include="..\src\localisation\real_names.c" />
|
||||
<ClCompile Include="..\src\localisation\user.c" />
|
||||
@@ -203,6 +204,14 @@
|
||||
<ClInclude Include="..\src\cmdline.h" />
|
||||
<ClInclude Include="..\src\common.h" />
|
||||
<ClInclude Include="..\src\config.h" />
|
||||
<ClInclude Include="..\src\core\Exception.hpp" />
|
||||
<ClInclude Include="..\src\core\FileStream.hpp" />
|
||||
<ClInclude Include="..\src\core\IDisposable.hpp" />
|
||||
<ClInclude Include="..\src\core\IStream.hpp" />
|
||||
<ClInclude Include="..\src\core\Math.hpp" />
|
||||
<ClInclude Include="..\src\core\Memory.hpp" />
|
||||
<ClInclude Include="..\src\core\StringBuilder.hpp" />
|
||||
<ClInclude Include="..\src\core\StringReader.hpp" />
|
||||
<ClInclude Include="..\src\cursors.h" />
|
||||
<ClInclude Include="..\src\diagnostic.h" />
|
||||
<ClInclude Include="..\src\drawing\drawing.h" />
|
||||
@@ -231,6 +240,7 @@
|
||||
<ClInclude Include="..\src\localisation\date.h" />
|
||||
<ClInclude Include="..\src\localisation\format_codes.h" />
|
||||
<ClInclude Include="..\src\localisation\language.h" />
|
||||
<ClInclude Include="..\src\localisation\LanguagePack.h" />
|
||||
<ClInclude Include="..\src\localisation\localisation.h" />
|
||||
<ClInclude Include="..\src\localisation\string_ids.h" />
|
||||
<ClInclude Include="..\src\management\award.h" />
|
||||
|
||||
@@ -53,6 +53,9 @@
|
||||
<Filter Include="Test\Ride">
|
||||
<UniqueIdentifier>{c6b9c169-ff2a-41df-9b1c-47d15763c3e2}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="Source\Core">
|
||||
<UniqueIdentifier>{28a808eb-9017-44cc-939b-f828fd1e2e7d}</UniqueIdentifier>
|
||||
</Filter>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="..\openrct2.exe">
|
||||
@@ -213,9 +216,6 @@
|
||||
<ClCompile Include="..\src\localisation\currency.c">
|
||||
<Filter>Source\Localisation</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\src\localisation\language.c">
|
||||
<Filter>Source\Localisation</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\src\ride\ride.c">
|
||||
<Filter>Source\Ride</Filter>
|
||||
</ClCompile>
|
||||
@@ -534,6 +534,12 @@
|
||||
<ClCompile Include="..\src\world\particle.c">
|
||||
<Filter>Source\World</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\src\localisation\LanguagePack.cpp">
|
||||
<Filter>Source\Localisation</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\src\localisation\language.cpp">
|
||||
<Filter>Source\Localisation</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="..\src\management\award.h">
|
||||
@@ -773,5 +779,32 @@
|
||||
<ClInclude Include="..\src\interface\chat.h">
|
||||
<Filter>Source\Interface</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\src\core\IStream.hpp">
|
||||
<Filter>Source\Core</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\src\core\IDisposable.hpp">
|
||||
<Filter>Source\Core</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\src\core\FileStream.hpp">
|
||||
<Filter>Source\Core</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\src\core\Exception.hpp">
|
||||
<Filter>Source\Core</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\src\localisation\LanguagePack.h">
|
||||
<Filter>Source\Localisation</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\src\core\Memory.hpp">
|
||||
<Filter>Source\Core</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\src\core\StringBuilder.hpp">
|
||||
<Filter>Source\Core</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\src\core\StringReader.hpp">
|
||||
<Filter>Source\Core</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\src\core\Math.hpp">
|
||||
<Filter>Source\Core</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -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
|
||||
19
src/core/Exception.hpp
Normal file
19
src/core/Exception.hpp
Normal file
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include <exception>
|
||||
#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;
|
||||
};
|
||||
94
src/core/FileStream.hpp
Normal file
94
src/core/FileStream.hpp
Normal file
@@ -0,0 +1,94 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL.h>
|
||||
#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.");
|
||||
}
|
||||
}
|
||||
};
|
||||
10
src/core/IDisposable.hpp
Normal file
10
src/core/IDisposable.hpp
Normal file
@@ -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;
|
||||
};
|
||||
75
src/core/IStream.hpp
Normal file
75
src/core/IStream.hpp
Normal file
@@ -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<typename T>
|
||||
void Read(T *value) {
|
||||
Read(value, sizeof(T));
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the size of the given type to the stream directly from the given address.
|
||||
*/
|
||||
template<typename T>
|
||||
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<typename T>
|
||||
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<typename T>
|
||||
void WriteValue(const T value) {
|
||||
Write(&value);
|
||||
}
|
||||
};
|
||||
|
||||
class IOException : public Exception {
|
||||
public:
|
||||
IOException(const char *message) : Exception(message) { }
|
||||
};
|
||||
23
src/core/Math.hpp
Normal file
23
src/core/Math.hpp
Normal file
@@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* Common mathematical functions.
|
||||
*/
|
||||
namespace Math {
|
||||
|
||||
template<typename T>
|
||||
T Min(T a, T b) {
|
||||
return a < b ? a : b;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
T Max(T a, T b) {
|
||||
return a > b ? a : b;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
T Clamp(T low, T x, T high) {
|
||||
return Min(Max(low, x), high);
|
||||
}
|
||||
|
||||
}
|
||||
64
src/core/Memory.hpp
Normal file
64
src/core/Memory.hpp
Normal file
@@ -0,0 +1,64 @@
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* Utility methods for memory management. Typically helpers and wrappers around the C standard library.
|
||||
*/
|
||||
namespace Memory {
|
||||
template<typename T>
|
||||
T *Allocate() {
|
||||
return (T*)malloc(sizeof(T));
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
T *Allocate(size_t size) {
|
||||
return (T*)malloc(size);
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
T *AllocateArray(size_t count) {
|
||||
return (T*)malloc(count * sizeof(T));
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
T *Reallocate(T *ptr, size_t size) {
|
||||
if (ptr == NULL)
|
||||
return (T*)malloc(size);
|
||||
else
|
||||
return (T*)realloc((void*)ptr, size);
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
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<typename T>
|
||||
void Free(T *ptr) {
|
||||
free((void*)ptr);
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
T *Copy(T *dst, const T *src, size_t size) {
|
||||
return (T*)memcpy((void*)dst, (const void*)src, size);
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
T *CopyArray(T *dst, const T *src, size_t count) {
|
||||
return (T*)memcpy((void*)dst, (const void*)src, count * sizeof(T));
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
T *Duplicate(const T *src, size_t size) {
|
||||
T *result = Allocate<T>(size);
|
||||
return Copy(result, src, size);
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
T *DuplicateArray(const T *src, size_t count) {
|
||||
T *result = AllocateArray<T>(count);
|
||||
return CopyArray(result, src, count);
|
||||
}
|
||||
}
|
||||
116
src/core/StringBuilder.hpp
Normal file
116
src/core/StringBuilder.hpp
Normal file
@@ -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<utf8>(_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<utf8>(_buffer, _capacity);
|
||||
}
|
||||
};
|
||||
58
src/core/StringReader.hpp
Normal file
58
src/core/StringReader.hpp
Normal file
@@ -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;
|
||||
};
|
||||
473
src/localisation/LanguagePack.cpp
Normal file
473
src/localisation/LanguagePack.cpp
Normal file
@@ -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 <SDL.h>
|
||||
|
||||
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<utf8>(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;
|
||||
}
|
||||
77
src/localisation/LanguagePack.h
Normal file
77
src/localisation/LanguagePack.h
Normal file
@@ -0,0 +1,77 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
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<const utf8*> _strings;
|
||||
std::vector<ObjectOverride> _objectOverrides;
|
||||
std::vector<ScenarioOverride> _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);
|
||||
};
|
||||
@@ -18,6 +18,10 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*****************************************************************************/
|
||||
|
||||
#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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user