1 /*
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/>.
6  */
7 
8 /** @file game_text.cpp Implementation of handling translated strings. */
9 
10 #include "../stdafx.h"
11 #include "../strgen/strgen.h"
12 #include "../debug.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"
18 #include "game.hpp"
19 #include "game_info.hpp"
20 
21 #include "table/strings.h"
22 
23 #include <stdarg.h>
24 #include <memory>
25 
26 #include "../safeguards.h"
27 
strgen_warning(const char * s,...)28 void CDECL strgen_warning(const char *s, ...)
29 {
30 	char buf[1024];
31 	va_list va;
32 	va_start(va, s);
33 	vseprintf(buf, lastof(buf), s, va);
34 	va_end(va);
35 	Debug(script, 0, "{}:{}: warning: {}", _file, _cur_line, buf);
36 	_warnings++;
37 }
38 
strgen_error(const char * s,...)39 void CDECL strgen_error(const char *s, ...)
40 {
41 	char buf[1024];
42 	va_list va;
43 	va_start(va, s);
44 	vseprintf(buf, lastof(buf), s, va);
45 	va_end(va);
46 	Debug(script, 0, "{}:{}: error: {}", _file, _cur_line, buf);
47 	_errors++;
48 }
49 
strgen_fatal(const char * s,...)50 void NORETURN CDECL strgen_fatal(const char *s, ...)
51 {
52 	char buf[1024];
53 	va_list va;
54 	va_start(va, s);
55 	vseprintf(buf, lastof(buf), s, va);
56 	va_end(va);
57 	Debug(script, 0, "{}:{}: FATAL: {}", _file, _cur_line, buf);
58 	throw std::exception();
59 }
60 
61 /**
62  * Read all the raw language strings from the given file.
63  * @param file The file to read from.
64  * @return The raw strings, or nullptr upon error.
65  */
ReadRawLanguageStrings(const std::string & file)66 LanguageStrings ReadRawLanguageStrings(const std::string &file)
67 {
68 	size_t to_read;
69 	FILE *fh = FioFOpenFile(file, "rb", GAME_DIR, &to_read);
70 	if (fh == nullptr) return LanguageStrings();
71 
72 	FileCloser fhClose(fh);
73 
74 	auto pos = file.rfind(PATHSEPCHAR);
75 	if (pos == std::string::npos) return LanguageStrings();
76 	std::string langname = file.substr(pos + 1);
77 
78 	/* Check for invalid empty filename */
79 	if (langname.empty() || langname.front() == '.') return LanguageStrings();
80 
81 	LanguageStrings ret(langname.substr(0, langname.find('.')));
82 
83 	char buffer[2048];
84 	while (to_read != 0 && fgets(buffer, sizeof(buffer), fh) != nullptr) {
85 		size_t len = strlen(buffer);
86 
87 		/* Remove trailing spaces/newlines from the string. */
88 		size_t i = len;
89 		while (i > 0 && (buffer[i - 1] == '\r' || buffer[i - 1] == '\n' || buffer[i - 1] == ' ')) i--;
90 		buffer[i] = '\0';
91 
92 		ret.lines.emplace_back(buffer, i);
93 
94 		if (len > to_read) {
95 			to_read = 0;
96 		} else {
97 			to_read -= len;
98 		}
99 	}
100 
101 	return ret;
102 }
103 
104 
105 /** A reader that simply reads using fopen. */
106 struct StringListReader : StringReader {
107 	StringList::const_iterator p;   ///< The current location of the iteration.
108 	StringList::const_iterator end; ///< The end of the iteration.
109 
110 	/**
111 	 * Create the reader.
112 	 * @param data        The data to fill during reading.
113 	 * @param strings     The language strings we are reading.
114 	 * @param master      Are we reading the master file?
115 	 * @param translation Are we reading a translation?
116 	 */
StringListReaderStringListReader117 	StringListReader(StringData &data, const LanguageStrings &strings, bool master, bool translation) :
118 			StringReader(data, strings.language.c_str(), master, translation), p(strings.lines.begin()), end(strings.lines.end())
119 	{
120 	}
121 
ReadLineStringListReader122 	char *ReadLine(char *buffer, const char *last) override
123 	{
124 		if (this->p == this->end) return nullptr;
125 
126 		strecpy(buffer, this->p->c_str(), last);
127 		this->p++;
128 
129 		return buffer;
130 	}
131 };
132 
133 /** Class for writing an encoded language. */
134 struct TranslationWriter : LanguageWriter {
135 	StringList &strings; ///< The encoded strings.
136 
137 	/**
138 	 * Writer for the encoded data.
139 	 * @param strings The string table to add the strings to.
140 	 */
TranslationWriterTranslationWriter141 	TranslationWriter(StringList &strings) : strings(strings)
142 	{
143 	}
144 
WriteHeaderTranslationWriter145 	void WriteHeader(const LanguagePackHeader *header)
146 	{
147 		/* We don't use the header. */
148 	}
149 
FinaliseTranslationWriter150 	void Finalise()
151 	{
152 		/* Nothing to do. */
153 	}
154 
WriteLengthTranslationWriter155 	void WriteLength(uint length)
156 	{
157 		/* We don't write the length. */
158 	}
159 
WriteTranslationWriter160 	void Write(const byte *buffer, size_t length)
161 	{
162 		this->strings.emplace_back((const char *)buffer, length);
163 	}
164 };
165 
166 /** Class for writing the string IDs. */
167 struct StringNameWriter : HeaderWriter {
168 	StringList &strings; ///< The string names.
169 
170 	/**
171 	 * Writer for the string names.
172 	 * @param strings The string table to add the strings to.
173 	 */
StringNameWriterStringNameWriter174 	StringNameWriter(StringList &strings) : strings(strings)
175 	{
176 	}
177 
WriteStringIDStringNameWriter178 	void WriteStringID(const char *name, int stringid)
179 	{
180 		if (stringid == (int)this->strings.size()) this->strings.emplace_back(name);
181 	}
182 
FinaliseStringNameWriter183 	void Finalise(const StringData &data)
184 	{
185 		/* Nothing to do. */
186 	}
187 };
188 
189 /**
190  * Scanner to find language files in a GameScript directory.
191  */
192 class LanguageScanner : protected FileScanner {
193 private:
194 	GameStrings *gs;
195 	std::string exclude;
196 
197 public:
198 	/** Initialise */
LanguageScanner(GameStrings * gs,const std::string & exclude)199 	LanguageScanner(GameStrings *gs, const std::string &exclude) : gs(gs), exclude(exclude) {}
200 
201 	/**
202 	 * Scan.
203 	 */
Scan(const char * directory)204 	void Scan(const char *directory)
205 	{
206 		this->FileScanner::Scan(".txt", directory, false);
207 	}
208 
AddFile(const std::string & filename,size_t basepath_length,const std::string & tar_filename)209 	bool AddFile(const std::string &filename, size_t basepath_length, const std::string &tar_filename) override
210 	{
211 		if (exclude == filename) return true;
212 
213 		auto ls = ReadRawLanguageStrings(filename);
214 		if (!ls.IsValid()) return false;
215 
216 		gs->raw_strings.push_back(std::move(ls));
217 		return true;
218 	}
219 };
220 
221 /**
222  * Load all translations that we know of.
223  * @return Container with all (compiled) translations.
224  */
LoadTranslations()225 GameStrings *LoadTranslations()
226 {
227 	const GameInfo *info = Game::GetInfo();
228 	std::string basename(info->GetMainScript());
229 	auto e = basename.rfind(PATHSEPCHAR);
230 	if (e == std::string::npos) return nullptr;
231 	basename.erase(e + 1);
232 
233 	std::string filename = basename + "lang" PATHSEP "english.txt";
234 	if (!FioCheckFileExists(filename, GAME_DIR)) return nullptr;
235 
236 	auto ls = ReadRawLanguageStrings(filename);
237 	if (!ls.IsValid()) return nullptr;
238 
239 	GameStrings *gs = new GameStrings();
240 	try {
241 		gs->raw_strings.push_back(std::move(ls));
242 
243 		/* Scan for other language files */
244 		LanguageScanner scanner(gs, filename);
245 		std::string ldir = basename + "lang" PATHSEP;
246 
247 		const std::string tar_filename = info->GetTarFile();
248 		TarList::iterator iter;
249 		if (!tar_filename.empty() && (iter = _tar_list[GAME_DIR].find(tar_filename)) != _tar_list[GAME_DIR].end()) {
250 			/* The main script is in a tar file, so find all files that
251 			 * are in the same tar and add them to the langfile scanner. */
252 			for (const auto &tar : _tar_filelist[GAME_DIR]) {
253 				/* Not in the same tar. */
254 				if (tar.second.tar_filename != iter->first) continue;
255 
256 				/* Check the path and extension. */
257 				if (tar.first.size() <= ldir.size() || tar.first.compare(0, ldir.size(), ldir) != 0) continue;
258 				if (tar.first.compare(tar.first.size() - 4, 4, ".txt") != 0) continue;
259 
260 				scanner.AddFile(tar.first, 0, tar_filename);
261 			}
262 		} else {
263 			/* Scan filesystem */
264 			scanner.Scan(ldir.c_str());
265 		}
266 
267 		gs->Compile();
268 		return gs;
269 	} catch (...) {
270 		delete gs;
271 		return nullptr;
272 	}
273 }
274 
275 /** Compile the language. */
Compile()276 void GameStrings::Compile()
277 {
278 	StringData data(32);
279 	StringListReader master_reader(data, this->raw_strings[0], true, false);
280 	master_reader.ParseFile();
281 	if (_errors != 0) throw std::exception();
282 
283 	this->version = data.Version();
284 
285 	StringNameWriter id_writer(this->string_names);
286 	id_writer.WriteHeader(data);
287 
288 	for (const auto &p : this->raw_strings) {
289 		data.FreeTranslation();
290 		StringListReader translation_reader(data, p, false, p.language != "english");
291 		translation_reader.ParseFile();
292 		if (_errors != 0) throw std::exception();
293 
294 		this->compiled_strings.emplace_back(p.language);
295 		TranslationWriter writer(this->compiled_strings.back().lines);
296 		writer.WriteLang(data);
297 	}
298 }
299 
300 /** The currently loaded game strings. */
301 GameStrings *_current_data = nullptr;
302 
303 /**
304  * Get the string pointer of a particular game string.
305  * @param id The ID of the game string.
306  * @return The encoded string.
307  */
GetGameStringPtr(uint id)308 const char *GetGameStringPtr(uint id)
309 {
310 	if (id >= _current_data->cur_language->lines.size()) return GetStringPtr(STR_UNDEFINED);
311 	return _current_data->cur_language->lines[id].c_str();
312 }
313 
314 /**
315  * Register the current translation to the Squirrel engine.
316  * @param engine The engine to update/
317  */
RegisterGameTranslation(Squirrel * engine)318 void RegisterGameTranslation(Squirrel *engine)
319 {
320 	delete _current_data;
321 	_current_data = LoadTranslations();
322 	if (_current_data == nullptr) return;
323 
324 	HSQUIRRELVM vm = engine->GetVM();
325 	sq_pushroottable(vm);
326 	sq_pushstring(vm, "GSText", -1);
327 	if (SQ_FAILED(sq_get(vm, -2))) return;
328 
329 	int idx = 0;
330 	for (const auto &p : _current_data->string_names) {
331 		sq_pushstring(vm, p.c_str(), -1);
332 		sq_pushinteger(vm, idx);
333 		sq_rawset(vm, -3);
334 		idx++;
335 	}
336 
337 	sq_pop(vm, 2);
338 
339 	ReconsiderGameScriptLanguage();
340 }
341 
342 /**
343  * Reconsider the game script language, so we use the right one.
344  */
ReconsiderGameScriptLanguage()345 void ReconsiderGameScriptLanguage()
346 {
347 	if (_current_data == nullptr) return;
348 
349 	char temp[MAX_PATH];
350 	strecpy(temp, _current_language->file, lastof(temp));
351 
352 	/* Remove the extension */
353 	char *l = strrchr(temp, '.');
354 	assert(l != nullptr);
355 	*l = '\0';
356 
357 	/* Skip the path */
358 	char *language = strrchr(temp, PATHSEPCHAR);
359 	assert(language != nullptr);
360 	language++;
361 
362 	for (auto &p : _current_data->compiled_strings) {
363 		if (p.language == language) {
364 			_current_data->cur_language = &p;
365 			return;
366 		}
367 	}
368 
369 	_current_data->cur_language = &_current_data->compiled_strings[0];
370 }
371