2 * This file is part of OpenTTD.
3 * 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.
4 * 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.
5 * 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 <http://www.gnu.org/licenses/>.
8 /** @file game_text.cpp Implementation of handling translated strings. */
10 #include "../stdafx.h"
11 #include "../strgen/strgen.h"
13 #include "../fileio_func.h"
14 #include "../tar_type.h"
15 #include "../script/squirrel_class.hpp"
16 #include "../strings_func.h"
17 #include "game_text.hpp"
19 #include "game_info.hpp"
21 #include "table/strings.h"
22 #include "table/strgen_tables.h"
24 #include "../safeguards.h"
26 void CDECL
StrgenWarningI(const std::string
&msg
)
28 Debug(script
, 0, "{}:{}: warning: {}", _file
, _cur_line
, msg
);
32 void CDECL
StrgenErrorI(const std::string
&msg
)
34 Debug(script
, 0, "{}:{}: error: {}", _file
, _cur_line
, msg
);
38 void CDECL
StrgenFatalI(const std::string
&msg
)
40 Debug(script
, 0, "{}:{}: FATAL: {}", _file
, _cur_line
, msg
);
41 throw std::exception();
45 * Read all the raw language strings from the given file.
46 * @param file The file to read from.
47 * @return The raw strings, or nullptr upon error.
49 LanguageStrings
ReadRawLanguageStrings(const std::string
&file
)
52 auto fh
= FioFOpenFile(file
, "rb", GAME_DIR
, &to_read
);
53 if (!fh
.has_value()) return LanguageStrings();
55 auto pos
= file
.rfind(PATHSEPCHAR
);
56 if (pos
== std::string::npos
) return LanguageStrings();
57 std::string langname
= file
.substr(pos
+ 1);
59 /* Check for invalid empty filename */
60 if (langname
.empty() || langname
.front() == '.') return LanguageStrings();
62 LanguageStrings
ret(langname
.substr(0, langname
.find('.')));
65 while (to_read
!= 0 && fgets(buffer
, sizeof(buffer
), *fh
) != nullptr) {
66 size_t len
= strlen(buffer
);
68 /* Remove trailing spaces/newlines from the string. */
70 while (i
> 0 && (buffer
[i
- 1] == '\r' || buffer
[i
- 1] == '\n' || buffer
[i
- 1] == ' ')) i
--;
73 ret
.lines
.emplace_back(buffer
, i
);
86 /** A reader that simply reads using fopen. */
87 struct StringListReader
: StringReader
{
88 StringList::const_iterator p
; ///< The current location of the iteration.
89 StringList::const_iterator end
; ///< The end of the iteration.
93 * @param data The data to fill during reading.
94 * @param strings The language strings we are reading.
95 * @param master Are we reading the master file?
96 * @param translation Are we reading a translation?
98 StringListReader(StringData
&data
, const LanguageStrings
&strings
, bool master
, bool translation
) :
99 StringReader(data
, strings
.language
.c_str(), master
, translation
), p(strings
.lines
.begin()), end(strings
.lines
.end())
103 std::optional
<std::string
> ReadLine() override
105 if (this->p
== this->end
) return std::nullopt
;
110 /** Class for writing an encoded language. */
111 struct TranslationWriter
: LanguageWriter
{
112 StringList
&strings
; ///< The encoded strings.
115 * Writer for the encoded data.
116 * @param strings The string table to add the strings to.
118 TranslationWriter(StringList
&strings
) : strings(strings
)
122 void WriteHeader(const LanguagePackHeader
*) override
124 /* We don't use the header. */
127 void Finalise() override
132 void WriteLength(uint
) override
134 /* We don't write the length. */
137 void Write(const uint8_t *buffer
, size_t length
) override
139 this->strings
.emplace_back((const char *)buffer
, length
);
143 /** Class for writing the string IDs. */
144 struct StringNameWriter
: HeaderWriter
{
145 StringList
&strings
; ///< The string names.
148 * Writer for the string names.
149 * @param strings The string table to add the strings to.
151 StringNameWriter(StringList
&strings
) : strings(strings
)
155 void WriteStringID(const std::string
&name
, int stringid
) override
157 if (stringid
== (int)this->strings
.size()) this->strings
.emplace_back(name
);
160 void Finalise(const StringData
&) override
167 * Scanner to find language files in a GameScript directory.
169 class LanguageScanner
: protected FileScanner
{
176 LanguageScanner(GameStrings
*gs
, const std::string
&exclude
) : gs(gs
), exclude(exclude
) {}
181 void Scan(const std::string
&directory
)
183 this->FileScanner::Scan(".txt", directory
, false);
186 bool AddFile(const std::string
&filename
, size_t, const std::string
&) override
188 if (exclude
== filename
) return true;
190 auto ls
= ReadRawLanguageStrings(filename
);
191 if (!ls
.IsValid()) return false;
193 gs
->raw_strings
.push_back(std::move(ls
));
199 * Load all translations that we know of.
200 * @return Container with all (compiled) translations.
202 GameStrings
*LoadTranslations()
204 const GameInfo
*info
= Game::GetInfo();
205 assert(info
!= nullptr);
206 std::string
basename(info
->GetMainScript());
207 auto e
= basename
.rfind(PATHSEPCHAR
);
208 if (e
== std::string::npos
) return nullptr;
209 basename
.erase(e
+ 1);
211 std::string filename
= basename
+ "lang" PATHSEP
"english.txt";
212 if (!FioCheckFileExists(filename
, GAME_DIR
)) return nullptr;
214 auto ls
= ReadRawLanguageStrings(filename
);
215 if (!ls
.IsValid()) return nullptr;
217 GameStrings
*gs
= new GameStrings();
219 gs
->raw_strings
.push_back(std::move(ls
));
221 /* Scan for other language files */
222 LanguageScanner
scanner(gs
, filename
);
223 std::string ldir
= basename
+ "lang" PATHSEP
;
225 const std::string tar_filename
= info
->GetTarFile();
226 TarList::iterator iter
;
227 if (!tar_filename
.empty() && (iter
= _tar_list
[GAME_DIR
].find(tar_filename
)) != _tar_list
[GAME_DIR
].end()) {
228 /* The main script is in a tar file, so find all files that
229 * are in the same tar and add them to the langfile scanner. */
230 for (const auto &tar
: _tar_filelist
[GAME_DIR
]) {
231 /* Not in the same tar. */
232 if (tar
.second
.tar_filename
!= iter
->first
) continue;
234 /* Check the path and extension. */
235 if (tar
.first
.size() <= ldir
.size() || tar
.first
.compare(0, ldir
.size(), ldir
) != 0) continue;
236 if (tar
.first
.compare(tar
.first
.size() - 4, 4, ".txt") != 0) continue;
238 scanner
.AddFile(tar
.first
, 0, tar_filename
);
241 /* Scan filesystem */
253 static StringParam::ParamType
GetParamType(const CmdStruct
*cs
)
255 if (cs
->value
== SCC_RAW_STRING_POINTER
) return StringParam::RAW_STRING
;
256 if (cs
->value
== SCC_STRING
|| cs
!= TranslateCmdForCompare(cs
)) return StringParam::STRING
;
257 return StringParam::OTHER
;
260 static void ExtractStringParams(const StringData
&data
, StringParamsList
¶ms
)
262 for (size_t i
= 0; i
< data
.max_strings
; i
++) {
263 const LangString
*ls
= data
.strings
[i
].get();
266 StringParams
¶m
= params
.emplace_back();
267 ParsedCommandStruct pcs
= ExtractCommandString(ls
->english
.c_str(), false);
269 for (auto it
= pcs
.consuming_commands
.begin(); it
!= pcs
.consuming_commands
.end(); it
++) {
270 if (*it
== nullptr) {
271 /* Skip empty param unless a non empty param exist after it. */
272 if (std::all_of(it
, pcs
.consuming_commands
.end(), [](auto cs
) { return cs
== nullptr; })) break;
273 param
.emplace_back(StringParam::UNUSED
, 1, nullptr);
276 const CmdStruct
*cs
= *it
;
277 param
.emplace_back(GetParamType(cs
), cs
->consumes
, cs
->cmd
);
283 /** Compile the language. */
284 void GameStrings::Compile()
287 StringListReader
master_reader(data
, this->raw_strings
[0], true, false);
288 master_reader
.ParseFile();
289 if (_errors
!= 0) throw std::exception();
291 this->version
= data
.Version();
293 ExtractStringParams(data
, this->string_params
);
295 StringNameWriter
id_writer(this->string_names
);
296 id_writer
.WriteHeader(data
);
298 for (const auto &p
: this->raw_strings
) {
299 data
.FreeTranslation();
300 StringListReader
translation_reader(data
, p
, false, p
.language
!= "english");
301 translation_reader
.ParseFile();
302 if (_errors
!= 0) throw std::exception();
304 this->compiled_strings
.emplace_back(p
.language
);
305 TranslationWriter
writer(this->compiled_strings
.back().lines
);
306 writer
.WriteLang(data
);
310 /** The currently loaded game strings. */
311 GameStrings
*_current_data
= nullptr;
314 * Get the string pointer of a particular game string.
315 * @param id The ID of the game string.
316 * @return The encoded string.
318 const char *GetGameStringPtr(uint id
)
320 if (_current_data
== nullptr || _current_data
->cur_language
== nullptr || id
>= _current_data
->cur_language
->lines
.size()) return GetStringPtr(STR_UNDEFINED
);
321 return _current_data
->cur_language
->lines
[id
].c_str();
325 * Get the string parameters of a particular game string.
326 * @param id The ID of the game string.
327 * @return The string parameters.
329 const StringParams
&GetGameStringParams(uint id
)
331 /* An empty result for STR_UNDEFINED. */
332 static StringParams empty
;
334 if (id
>= _current_data
->string_params
.size()) return empty
;
335 return _current_data
->string_params
[id
];
339 * Get the name of a particular game string.
340 * @param id The ID of the game string.
341 * @return The name of the string.
343 const std::string
&GetGameStringName(uint id
)
345 /* The name for STR_UNDEFINED. */
346 static const std::string undefined
= "STR_UNDEFINED";
348 if (id
>= _current_data
->string_names
.size()) return undefined
;
349 return _current_data
->string_names
[id
];
353 * Register the current translation to the Squirrel engine.
354 * @param engine The engine to update/
356 void RegisterGameTranslation(Squirrel
*engine
)
358 delete _current_data
;
359 _current_data
= LoadTranslations();
360 if (_current_data
== nullptr) return;
362 HSQUIRRELVM vm
= engine
->GetVM();
363 sq_pushroottable(vm
);
364 sq_pushstring(vm
, "GSText", -1);
365 if (SQ_FAILED(sq_get(vm
, -2))) return;
368 for (const auto &p
: _current_data
->string_names
) {
369 sq_pushstring(vm
, p
, -1);
370 sq_pushinteger(vm
, idx
);
377 ReconsiderGameScriptLanguage();
381 * Reconsider the game script language, so we use the right one.
383 void ReconsiderGameScriptLanguage()
385 if (_current_data
== nullptr) return;
387 std::string language
= FS2OTTD(_current_language
->file
.stem());
388 for (auto &p
: _current_data
->compiled_strings
) {
389 if (p
.language
== language
) {
390 _current_data
->cur_language
= &p
;
395 _current_data
->cur_language
= &_current_data
->compiled_strings
[0];