diff --git a/projects/openttd_vs100.vcxproj b/projects/openttd_vs100.vcxproj index 716a1ae30f..20acbb0e51 100644 --- a/projects/openttd_vs100.vcxproj +++ b/projects/openttd_vs100.vcxproj @@ -358,6 +358,7 @@ + @@ -560,6 +561,7 @@ + @@ -931,6 +933,8 @@ + + diff --git a/projects/openttd_vs100.vcxproj.filters b/projects/openttd_vs100.vcxproj.filters index 10c88fbf46..fb0898f7c2 100644 --- a/projects/openttd_vs100.vcxproj.filters +++ b/projects/openttd_vs100.vcxproj.filters @@ -303,6 +303,9 @@ Source Files + + Source Files + Source Files @@ -909,6 +912,9 @@ Header Files + + Header Files + Header Files @@ -2022,6 +2028,12 @@ Game Core + + Game Core + + + Game Core + Script API diff --git a/projects/openttd_vs80.vcproj b/projects/openttd_vs80.vcproj index 0473d53ffe..7f09ce3a25 100644 --- a/projects/openttd_vs80.vcproj +++ b/projects/openttd_vs80.vcproj @@ -702,6 +702,10 @@ RelativePath=".\..\src\station.cpp" > + + @@ -1514,6 +1518,10 @@ RelativePath=".\..\src\stdafx.h" > + + @@ -3054,6 +3062,14 @@ RelativePath=".\..\src\game\game_scanner.hpp" > + + + + + + @@ -1511,6 +1515,10 @@ RelativePath=".\..\src\stdafx.h" > + + @@ -3051,6 +3059,14 @@ RelativePath=".\..\src\game\game_scanner.hpp" > + + + + +class AutoDeleteSmallVector : public SmallVector { +public: + ~AutoDeleteSmallVector() + { + this->Clear(); + } + + /** + * Remove all items from the list. + */ + FORCEINLINE void Clear() + { + for (uint i = 0; i < this->items; i++) { + delete this->data[i]; + } + + this->items = 0; + } +}; + typedef AutoFreeSmallVector StringList; ///< Type for a list of strings. #endif /* SMALLVEC_TYPE_HPP */ diff --git a/src/game/game.hpp b/src/game/game.hpp index 9e13bc5b63..50d97b50c6 100644 --- a/src/game/game.hpp +++ b/src/game/game.hpp @@ -91,6 +91,11 @@ public: */ static class GameInstance *GetInstance() { return Game::instance; } + /** + * Get the current active mainscript. + */ + static const char *GetMainScript(); + #if defined(ENABLE_NETWORK) /** Wrapper function for GameScanner::HasGame */ static bool HasGame(const struct ContentInfo *ci, bool md5sum); diff --git a/src/game/game_core.cpp b/src/game/game_core.cpp index ac24853da4..2816a62232 100644 --- a/src/game/game_core.cpp +++ b/src/game/game_core.cpp @@ -22,6 +22,7 @@ #include "game_scanner.hpp" #include "game_config.hpp" #include "game_instance.hpp" +#include "game_info.hpp" /* static */ uint Game::frame_counter = 0; /* static */ GameInfo *Game::info = NULL; @@ -29,6 +30,11 @@ /* static */ GameScannerInfo *Game::scanner_info = NULL; /* static */ GameScannerLibrary *Game::scanner_library = NULL; +/* static */ const char *Game::GetMainScript() +{ + return Game::info->GetMainScript(); +} + /* static */ void Game::GameLoop() { if (_networking && !_network_server) return; diff --git a/src/game/game_instance.cpp b/src/game/game_instance.cpp index 81870603b0..e4971238a8 100644 --- a/src/game/game_instance.cpp +++ b/src/game/game_instance.cpp @@ -19,6 +19,7 @@ #include "game_config.hpp" #include "game_info.hpp" #include "game_instance.hpp" +#include "game_text.hpp" #include "game.hpp" /* Convert all Game related classes to Squirrel data. @@ -180,6 +181,7 @@ void GameInstance::RegisterAPI() SQGSWaypointList_Vehicle_Register(this->engine); SQGSWindow_Register(this->engine); + RegisterGameTranslation(this->engine); } int GameInstance::GetSetting(const char *name) diff --git a/src/game/game_text.cpp b/src/game/game_text.cpp new file mode 100644 index 0000000000..0b41579f38 --- /dev/null +++ b/src/game/game_text.cpp @@ -0,0 +1,384 @@ +/* $Id$ */ + +/* + * This file is part of OpenTTD. + * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2. + * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see . + */ + +/** @file game_text.cpp Implementation of handling translated strings. */ + +#include "../stdafx.h" +#include "../language.h" +#include "../strgen/strgen.h" +#include "../debug.h" +#include "../fileio_func.h" +#include "../script/squirrel_class.hpp" +#include "../strings_func.h" +#include "game_text.hpp" +#include "game.hpp" + +#include "table/strings.h" + +#include +#include + +void CDECL strgen_warning(const char *s, ...) +{ + char buf[1024]; + va_list va; + va_start(va, s); + vsnprintf(buf, lengthof(buf), s, va); + va_end(va); + DEBUG(script, 0, "%s:%d: warning: %s", _file, _cur_line, buf); + _warnings++; +} + +void CDECL strgen_error(const char *s, ...) +{ + char buf[1024]; + va_list va; + va_start(va, s); + vsnprintf(buf, lengthof(buf), s, va); + va_end(va); + DEBUG(script, 0, "%s:%d: error: %s", _file, _cur_line, buf); + _errors++; +} + +void NORETURN CDECL strgen_fatal(const char *s, ...) +{ + char buf[1024]; + va_list va; + va_start(va, s); + vsnprintf(buf, lengthof(buf), s, va); + va_end(va); + DEBUG(script, 0, "%s:%d: FATAL: %s", _file, _cur_line, buf); + throw std::exception(); +} + +/** + * Create a new container for language strings. + * @param language The language name. + */ +LanguageStrings::LanguageStrings(const char *language) +{ + const char *p = strrchr(language, PATHSEPCHAR); + if (p == NULL) { + p = language; + } else { + p++; + } + + const char *e = strchr(p, '.'); + this->language = e == NULL ? strdup(p) : strndup(p, e - p); +} + +/** Free everything. */ +LanguageStrings::~LanguageStrings() +{ + free(this->language); +} + +/** + * Read all the raw language strings from the given file. + * @param file The file to read from. + * @return The raw strings, or NULL upon error. + */ +LanguageStrings *ReadRawLanguageStrings(const char *file) +{ + LanguageStrings *ret = NULL; + try { + size_t to_read; + FILE *fh = FioFOpenFile(file, "rb", GAME_DIR, &to_read); + if (fh == NULL) { + return NULL; + } + + ret = new LanguageStrings(file); + + char buffer[2048]; + while (to_read != 0 && fgets(buffer, sizeof(buffer), fh) != NULL) { + size_t len = strlen(buffer); + + /* Remove trailing spaces/newlines from the string. */ + size_t i = len; + while (i > 0 && (buffer[i - 1] == '\r' || buffer[i - 1] == '\n' || buffer[i - 1] == ' ')) i--; + buffer[i] = '\0'; + + *ret->lines.Append() = strndup(buffer, to_read); + + if (len > to_read) { + to_read = 0; + } else { + to_read -= len; + } + } + + return ret; + } catch (...) { + delete ret; + return NULL; + } +} + + +/** A reader that simply reads using fopen. */ +struct StringListReader : StringReader { + const char * const *p; ///< The current location of the iteration. + const char * const *end; ///< The end of the iteration. + + /** + * Create the reader. + * @param data The data to fill during reading. + * @param file The file we are reading. + * @param master Are we reading the master file? + * @param translation Are we reading a translation? + */ + StringListReader(StringData &data, const LanguageStrings *strings, bool master, bool translation) : + StringReader(data, strings->language, master, translation), p(strings->lines.Begin()), end(strings->lines.End()) + { + } + + /* virtual */ char *ReadLine(char *buffer, size_t size) + { + if (this->p == this->end) return NULL; + + strncpy(buffer, *this->p, size); + this->p++; + + return buffer; + } + + /* virtual */ void HandlePragma(char *str) + { + strgen_fatal("unknown pragma '%s'", str); + } +}; + +/** Class for writing an encoded language. */ +struct TranslationWriter : LanguageWriter { + StringList *strings; ///< The encoded strings. + + /** + * Writer for the encoded data. + * @param strings The string table to add the strings to. + */ + TranslationWriter(StringList *strings) : strings(strings) + { + } + + void WriteHeader(const LanguagePackHeader *header) + { + /* We don't use the header. */ + } + + void Finalise() + { + /* Nothing to do. */ + } + + void WriteLength(uint length) + { + /* We don't write the length. */ + } + + void Write(const byte *buffer, size_t length) + { + *this->strings->Append() = strndup((const char*)buffer, length); + } +}; + +/** Class for writing the string IDs. */ +struct StringNameWriter : HeaderWriter { + StringList *strings; ///< The string names. + + /** + * Writer for the string names. + * @param strings The string table to add the strings to. + */ + StringNameWriter(StringList *strings) : strings(strings) + { + } + + void WriteStringID(const char *name, int stringid) + { + if (stringid == (int)this->strings->Length()) *this->strings->Append() = strdup(name); + } + + void Finalise(const StringData &data) + { + /* Nothing to do. */ + } +}; + +static void GetBasePath(char *buffer, size_t length) +{ + strecpy(buffer, Game::GetMainScript(), buffer + length); + char *s = strrchr(buffer, PATHSEPCHAR); + if (s != NULL) { + /* Keep the PATHSEPCHAR there, remove the rest */ + s++; + *s = '\0'; + } + + /* Tars dislike opening files with '/' on Windows.. so convert it to '\\' */ +#if (PATHSEPCHAR != '/') + for (char *n = buffer; *n != '\0'; n++) if (*n == '/') *n = PATHSEPCHAR; +#endif +} + +/** + * Scanner to find language files in a GameScript directory. + */ +class LanguageScanner : protected FileScanner { +private: + GameStrings *gs; + char *exclude; + +public: + /** Initialise */ + LanguageScanner(GameStrings *gs, const char *exclude) : gs(gs), exclude(strdup(exclude)) {} + ~LanguageScanner() { free(exclude); } + + /** + * Scan. + */ + void Scan(const char *directory) + { + this->FileScanner::Scan(".txt", directory, false); + } + + /* virtual */ bool AddFile(const char *filename, size_t basepath_length, const char *tar_filename) + { + if (strcmp(filename, exclude) == 0) return true; + + *gs->raw_strings.Append() = ReadRawLanguageStrings(filename); + return true; + } +}; + +/** + * Load all translations that we know of. + * @return Container with all (compiled) translations. + */ +GameStrings *LoadTranslations() +{ + GameStrings *gs = new GameStrings(); + try { + char filename[512]; + GetBasePath(filename, sizeof(filename)); + char *e = filename + strlen(filename); + + seprintf(e, filename + sizeof(filename), "lang" PATHSEP "english.txt"); + if (!FioCheckFileExists(filename, GAME_DIR)) throw std::exception(); + *gs->raw_strings.Append() = ReadRawLanguageStrings(filename); + + /* Scan for other language files */ + LanguageScanner scanner(gs, filename); + strecpy(e, "lang" PATHSEP, filename + sizeof(filename)); + scanner.Scan(filename); + + gs->Compile(); + return gs; + } catch (...) { + delete gs; + return NULL; + } +} + +/** Compile the language. */ +void GameStrings::Compile() +{ + StringData data(1); + StringListReader master_reader(data, this->raw_strings[0], true, false); + master_reader.ParseFile(); + if (_errors != 0) throw std::exception(); + + this->version = data.Version(); + + StringNameWriter id_writer(&this->string_names); + id_writer.WriteHeader(data); + + for (LanguageStrings **p = this->raw_strings.Begin(); p != this->raw_strings.End(); p++) { + data.FreeTranslation(); + StringListReader translation_reader(data, *p, false, strcmp((*p)->language, "english") != 0); + translation_reader.ParseFile(); + if (_errors != 0) throw std::exception(); + + LanguageStrings *compiled = *this->compiled_strings.Append() = new LanguageStrings((*p)->language); + TranslationWriter writer(&compiled->lines); + writer.WriteLang(data); + } +} + +/** The currently loaded game strings. */ +GameStrings *_current_data = NULL; + +/** + * Get the string pointer of a particular game string. + * @param id The ID of the game string. + * @return The encoded string. + */ +const char *GetGameStringPtr(uint id) +{ + if (id >= _current_data->cur_language->lines.Length()) return GetStringPtr(STR_UNDEFINED); + return _current_data->cur_language->lines[id]; +} + +/** + * Register the current translation to the Squirrel engine. + * @param engine The engine to update/ + */ +void RegisterGameTranslation(Squirrel *engine) +{ + delete _current_data; + _current_data = LoadTranslations(); + if (_current_data == NULL) return; + + HSQUIRRELVM vm = engine->GetVM(); + sq_pushroottable(vm); + sq_pushstring(vm, _SC("GSText"), -1); + if (SQ_FAILED(sq_get(vm, -2))) return; + + int idx = 0; + for (const char * const *p = _current_data->string_names.Begin(); p != _current_data->string_names.End(); p++, idx++) { + sq_pushstring(vm, OTTD2SQ(*p), -1); + sq_pushinteger(vm, idx); + sq_rawset(vm, -3); + } + + sq_pop(vm, 2); + + ReconsiderGameScriptLanguage(); +} + +/** + * Reconsider the game script language, so we use the right one. + */ +void ReconsiderGameScriptLanguage() +{ + if (_current_data == NULL) return; + + char temp[MAX_PATH]; + strecpy(temp, _current_language->file, temp + sizeof(temp)); + + /* Remove the extension */ + char *l = strrchr(temp, '.'); + assert(l != NULL); + *l = '\0'; + + /* Skip the path */ + char *language = strrchr(temp, PATHSEPCHAR); + assert(language != NULL); + language++; + + for (LanguageStrings **p = _current_data->compiled_strings.Begin(); p != _current_data->compiled_strings.End(); p++) { + if (strcmp((*p)->language, language) == 0) { + _current_data->cur_language = *p; + return; + } + } + + _current_data->cur_language = _current_data->compiled_strings[0]; +} diff --git a/src/game/game_text.hpp b/src/game/game_text.hpp new file mode 100644 index 0000000000..b367e63f9e --- /dev/null +++ b/src/game/game_text.hpp @@ -0,0 +1,45 @@ +/* $Id$ */ + +/* + * This file is part of OpenTTD. + * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2. + * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see . + */ + +/** @file game_text.hpp Base functions regarding game texts. */ + +#ifndef GAME_TEXT_HPP +#define GAME_TEXT_HPP + +#include "../core/smallvec_type.hpp" + +/** The tab we place our strings in. */ +static const uint GAME_TEXT_TAB = 18; + +const char *GetGameStringPtr(uint id); +void RegisterGameTranslation(class Squirrel *engine); +void ReconsiderGameScriptLanguage(); + +/** Container for the raw (unencoded) language strings of a language. */ +struct LanguageStrings { + const char *language; ///< Name of the language (base filename). + StringList lines; ///< The lines of the file to pass into the parser/encoder. + + LanguageStrings(const char *language); + ~LanguageStrings(); +}; + +/** Container for all the game strings. */ +struct GameStrings { + uint version; ///< The version of the language strings. + LanguageStrings *cur_language; ///< The current (compiled) language. + + AutoDeleteSmallVector raw_strings; ///< The raw strings per language, first must be English/the master language!. + AutoDeleteSmallVector compiled_strings; ///< The compiled strings per language, first must be English/the master language!. + StringList string_names; ///< The names of the compiled strings. + + void Compile(); +}; + +#endif /* GAME_TEXT_HPP */ diff --git a/src/saveload/game_sl.cpp b/src/saveload/game_sl.cpp index c479239c10..6b93a46ea9 100644 --- a/src/saveload/game_sl.cpp +++ b/src/saveload/game_sl.cpp @@ -19,6 +19,7 @@ #include "../game/game_config.hpp" #include "../network/network.h" #include "../game/game_instance.hpp" +#include "../game/game_text.hpp" static char _game_saveload_name[64]; static int _game_saveload_version; @@ -111,6 +112,67 @@ static void Save_GSDT() SlAutolength((AutolengthProc *)SaveReal_GSDT, NULL); } +extern GameStrings *_current_data; + +static const char *_game_saveload_string; +static uint _game_saveload_strings; + +static const SaveLoad _game_language_header[] = { + SLEG_STR(_game_saveload_string, SLE_STR), + SLEG_VAR(_game_saveload_strings, SLE_UINT32), + SLE_END() +}; + +static const SaveLoad _game_language_string[] = { + SLEG_STR(_game_saveload_string, SLE_STR | SLF_ALLOW_CONTROL), + SLE_END() +}; + +static void SaveReal_GSTR(LanguageStrings *ls) +{ + _game_saveload_string = ls->language; + _game_saveload_strings = ls->lines.Length(); + + SlObject(NULL, _game_language_header); + for (uint i = 0; i < _game_saveload_strings; i++) { + _game_saveload_string = ls->lines[i]; + SlObject(NULL, _game_language_string); + } +} + +static void Load_GSTR() +{ + delete _current_data; + _current_data = new GameStrings(); + + while (SlIterateArray() != -1) { + _game_saveload_string = NULL; + SlObject(NULL, _game_language_header); + + LanguageStrings *ls = new LanguageStrings(_game_saveload_string); + for (uint i = 0; i < _game_saveload_strings; i++) { + SlObject(NULL, _game_language_string); + *ls->lines.Append() = strdup(_game_saveload_string); + } + + *_current_data->raw_strings.Append() = ls; + } + + _current_data->Compile(); + ReconsiderGameScriptLanguage(); +} + +static void Save_GSTR() +{ + if (_current_data == NULL) return; + + for (uint i = 0; i < _current_data->raw_strings.Length(); i++) { + SlSetArrayIndex(i); + SlAutolength((AutolengthProc *)SaveReal_GSTR, _current_data->raw_strings[i]); + } +} + extern const ChunkHandler _game_chunk_handlers[] = { + { 'GSTR', Save_GSTR, Load_GSTR, NULL, NULL, CH_ARRAY }, { 'GSDT', Save_GSDT, Load_GSDT, NULL, NULL, CH_ARRAY | CH_LAST}, }; diff --git a/src/strings.cpp b/src/strings.cpp index 907c1e4d4e..5e7809c25d 100644 --- a/src/strings.cpp +++ b/src/strings.cpp @@ -38,6 +38,7 @@ #include "smallmap_gui.h" #include "window_func.h" #include "debug.h" +#include "game/game_text.hpp" #include #include "table/strings.h" @@ -140,6 +141,7 @@ static bool _keep_gender_data = false; ///< Should we retain the gender data in const char *GetStringPtr(StringID string) { switch (GB(string, TAB_COUNT_OFFSET, TAB_COUNT_BITS)) { + case GAME_TEXT_TAB: return GetGameStringPtr(GB(string, TAB_SIZE_OFFSET, TAB_SIZE_BITS)); /* GetGRFStringPtr doesn't handle 0xD4xx ids, we need to convert those to 0xD0xx. */ case 26: return GetStringPtr(GetGRFStringID(0, 0xD000 + GB(string, TAB_SIZE_OFFSET, 10))); case 28: return GetGRFStringPtr(GB(string, TAB_SIZE_OFFSET, TAB_SIZE_BITS)); @@ -182,6 +184,9 @@ char *GetStringWithArgs(char *buffr, StringID string, StringParameters *args, co /* Old table for custom names. This is no longer used */ error("Incorrect conversion of custom name string."); + case GAME_TEXT_TAB: + return FormatString(buffr, GetGameStringPtr(index), args, last, case_index); + case 26: /* Include string within newgrf text (format code 81) */ if (HasBit(index, 10)) { @@ -1611,6 +1616,7 @@ bool ReadLanguagePack(const LanguageMetadata *lang) #endif /* WITH_ICU */ /* Some lists need to be sorted again after a language change. */ + ReconsiderGameScriptLanguage(); InitializeSortedCargoSpecs(); SortIndustryTypes(); BuildIndustriesLegend();