mirror of
https://github.com/OpenRCT2/OpenRCT2
synced 2026-01-18 12:33:17 +01:00
When loading language pack, a check is made not to double entries. This check was wrong, because it tried to use offsets as pointers to strings. These offsets are later rewritten to actual pointers in remaining part of LanguagePack::LanguagePack
496 lines
13 KiB
C++
496 lines
13 KiB
C++
extern "C" {
|
|
#include "../common.h"
|
|
#include "../util/util.h"
|
|
#include "localisation.h"
|
|
#include "../platform/platform.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 != nullptr);
|
|
|
|
uint32 fileLength;
|
|
utf8 *fileData = nullptr;
|
|
|
|
// Load file directly into memory
|
|
try {
|
|
FileStream fs = FileStream(path, FILE_MODE_OPEN);
|
|
|
|
fileLength = (uint32)fs.GetLength();
|
|
fileData = Memory::Allocate<utf8>(fileLength + 1);
|
|
fs.Read(fileData, fileLength);
|
|
fileData[fileLength] = '\0';
|
|
|
|
fs.Dispose();
|
|
} catch (Exception ex) {
|
|
Memory::Free(fileData);
|
|
log_error("Unable to open %s: %s", path, ex.GetMessage());
|
|
return nullptr;
|
|
}
|
|
|
|
// 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 != nullptr);
|
|
|
|
_id = id;
|
|
_stringData = nullptr;
|
|
_currentGroup = nullptr;
|
|
_currentObjectOverride = nullptr;
|
|
_currentScenarioOverride = nullptr;
|
|
|
|
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 != nullptr) {
|
|
*strPtr = (utf8*)(stringDataBaseAddress + (size_t)*strPtr);
|
|
}
|
|
}
|
|
}
|
|
for (size_t i = 0; i < _scenarioOverrides.size(); i++) {
|
|
for (int j = 0; j < ScenarioOverrideMaxStringCount; j++) {
|
|
const utf8 **strPtr = &(_scenarioOverrides[i].strings[j]);
|
|
if (*strPtr != nullptr) {
|
|
*strPtr = (utf8*)(stringDataBaseAddress + (size_t)*strPtr);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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 nullptr;
|
|
}
|
|
}else if (stringId >= ObjectOverrideBase) {
|
|
int offset = stringId - ObjectOverrideBase;
|
|
int ooIndex = offset / ObjectOverrideMaxStringCount;
|
|
int ooStringIndex = offset % ObjectOverrideMaxStringCount;
|
|
|
|
if (_objectOverrides.size() > (size_t)ooIndex) {
|
|
return _objectOverrides[ooIndex].strings[ooStringIndex];
|
|
} else {
|
|
return nullptr;
|
|
}
|
|
} else {
|
|
if (_strings.size() > (size_t)stringId) {
|
|
return _strings[stringId];
|
|
} else {
|
|
return nullptr;
|
|
}
|
|
}
|
|
}
|
|
|
|
rct_string_id LanguagePack::GetObjectOverrideStringId(const char *objectIdentifier, int index)
|
|
{
|
|
assert(objectIdentifier != nullptr);
|
|
assert(index < ObjectOverrideMaxStringCount);
|
|
|
|
int ooIndex = 0;
|
|
for (const ObjectOverride &objectOverride : _objectOverrides) {
|
|
if (strncmp(objectOverride.name, objectIdentifier, 8) == 0) {
|
|
if (objectOverride.strings[index] == nullptr) {
|
|
return STR_NONE;
|
|
}
|
|
return ObjectOverrideBase + (ooIndex * ObjectOverrideMaxStringCount) + index;
|
|
}
|
|
ooIndex++;
|
|
}
|
|
|
|
return STR_NONE;
|
|
}
|
|
|
|
rct_string_id LanguagePack::GetScenarioOverrideStringId(const utf8 *scenarioFilename, int index)
|
|
{
|
|
assert(scenarioFilename != nullptr);
|
|
assert(index < ScenarioOverrideMaxStringCount);
|
|
|
|
int ooIndex = 0;
|
|
for (const ScenarioOverride &scenarioOverride : _scenarioOverrides) {
|
|
if (_stricmp(scenarioOverride.filename, scenarioFilename) == 0) {
|
|
if (scenarioOverride.strings[index] == nullptr) {
|
|
return STR_NONE;
|
|
}
|
|
return ScenarioOverrideBase + (ooIndex * ScenarioOverrideMaxStringCount) + index;
|
|
}
|
|
ooIndex++;
|
|
}
|
|
|
|
return STR_NONE;
|
|
}
|
|
|
|
LanguagePack::ObjectOverride *LanguagePack::GetObjectOverride(const char *objectIdentifier)
|
|
{
|
|
assert(objectIdentifier != nullptr);
|
|
|
|
for (size_t i = 0; i < _objectOverrides.size(); i++) {
|
|
ObjectOverride *oo = &_objectOverrides[i];
|
|
if (strncmp(oo->name, objectIdentifier, 8) == 0) {
|
|
return oo;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
LanguagePack::ScenarioOverride *LanguagePack::GetScenarioOverride(const utf8 *scenarioIdentifier)
|
|
{
|
|
assert(scenarioIdentifier != nullptr);
|
|
|
|
for (size_t i = 0; i < _scenarioOverrides.size(); i++) {
|
|
ScenarioOverride *so = &_scenarioOverrides[i];
|
|
// At this point ScenarioOverrides were not yet rewritten to point at
|
|
// strings, but rather still hold offsets from base.
|
|
const utf8 *name = _stringDataSB.GetBuffer() + (size_t)so->name;
|
|
if (_stricmp(name, scenarioIdentifier) == 0) {
|
|
return so;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// Parsing
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// Partial support to open a uncompiled language file which parses tokens and converts them to the corresponding character
|
|
// code. Due to resource strings (strings in scenarios and objects) being written to the original game's string table,
|
|
// get_string will use those if the same entry in the loaded language is empty.
|
|
//
|
|
// Unsure at how the original game decides which entries to write resource strings to, but this could affect adding new
|
|
// strings for the time being. Further investigation is required.
|
|
//
|
|
// When reading the language files, the STR_XXXX part is read and XXXX becomes the string id number. Everything after the colon
|
|
// and before the new line will be saved as the string. Tokens are written with inside curly braces {TOKEN}.
|
|
// Use # at the beginning of a line to leave a comment.
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
static bool IsWhitespace(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 = nullptr;
|
|
if (_currentObjectOverride == nullptr) {
|
|
_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 = nullptr;
|
|
_currentScenarioOverride = GetScenarioOverride(_currentGroup);
|
|
if (_currentScenarioOverride == nullptr) {
|
|
_scenarioOverrides.push_back(ScenarioOverride());
|
|
_currentScenarioOverride = &_scenarioOverrides[_scenarioOverrides.size() - 1];
|
|
memset(_currentScenarioOverride, 0, sizeof(ScenarioOverride));
|
|
_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 == nullptr) {
|
|
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;
|
|
bool isByte;
|
|
if (ParseToken(reader, &token, &isByte)) {
|
|
if (isByte) {
|
|
sb.Append((const utf8*)&token, 1);
|
|
} else {
|
|
sb.Append((int)token);
|
|
}
|
|
} else {
|
|
// Syntax error or unknown token, ignore line entirely
|
|
return;
|
|
}
|
|
} else {
|
|
reader->Skip();
|
|
sb.Append(codepoint);
|
|
}
|
|
}
|
|
|
|
// Append a null terminator for the benefit of the last string
|
|
_stringDataSB.Append(0);
|
|
|
|
// Get the relative offset to the string (add the base offset when we extract the string properly)
|
|
utf8 *relativeOffset = (utf8*)_stringDataSB.GetLength();
|
|
|
|
if (_currentGroup == nullptr) {
|
|
// Make sure the list is big enough to contain this string id
|
|
while (_strings.size() <= (size_t)stringId) {
|
|
_strings.push_back(nullptr);
|
|
}
|
|
|
|
_strings[stringId] = relativeOffset;
|
|
} else {
|
|
if (_currentObjectOverride != nullptr) {
|
|
_currentObjectOverride->strings[stringId] = relativeOffset;
|
|
} else {
|
|
_currentScenarioOverride->strings[stringId] = relativeOffset;
|
|
}
|
|
}
|
|
|
|
_stringDataSB.Append(&sb);
|
|
}
|
|
|
|
bool LanguagePack::ParseToken(IStringReader *reader, uint32 *token, bool *isByte)
|
|
{
|
|
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);
|
|
*isByte = false;
|
|
|
|
// Handle explicit byte values
|
|
if (*token == 0) {
|
|
int number;
|
|
if (sscanf(tokenName, "%d", &number) == 1) {
|
|
*token = Math::Clamp(0, number, 255);
|
|
*isByte = true;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|