OpenTTD Source 20260711-master-g3fb3006dff
newgrf_text.cpp
Go to the documentation of this file.
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 <https://www.gnu.org/licenses/old-licenses/gpl-2.0>.
6 */
7
17
18#include "stdafx.h"
19
20#include "debug.h"
21#include "newgrf.h"
22#include "strings_internal.h"
23#include "newgrf_storage.h"
24#include "newgrf_text.h"
25#include "newgrf_cargo.h"
26#include "string_func.h"
28#include "debug.h"
31#include "language.h"
32#include <ranges>
33
34#include "table/strings.h"
35#include "table/control_codes.h"
36
37#include "safeguards.h"
38
41
42
49 GRFTextList textholder;
50 StringID def_string;
51 GrfID grfid;
52 GRFStringID stringid;
53};
54
55
57
64int LanguageMap::GetMapping(int newgrf_id, bool gender) const
65{
66 const std::vector<Mapping> &map = gender ? this->gender_map : this->case_map;
67 for (const Mapping &m : map) {
68 if (m.newgrf_id == newgrf_id) return m.openttd_id;
69 }
70 return -1;
71}
72
79int LanguageMap::GetReverseMapping(int openttd_id, bool gender) const
80{
81 const std::vector<Mapping> &map = gender ? this->gender_map : this->case_map;
82 for (const Mapping &m : map) {
83 if (m.openttd_id == openttd_id) return m.newgrf_id;
84 }
85 return -1;
86}
87
99
101 int offset;
102
104 std::map<int, std::string> strings;
105
111 void Flush(const LanguageMap *lm, std::string &dest)
112 {
113 if (this->strings.find(0) == this->strings.end()) {
114 /* In case of a (broken) NewGRF without a default,
115 * assume an empty string. */
116 GrfMsg(1, "choice list misses default value");
117 this->strings[0] = std::string();
118 }
119
120 StringBuilder builder(dest);
121
122 if (lm == nullptr) {
123 /* In case there is no mapping, just ignore everything but the default.
124 * A probable cause for this happening is when the language file has
125 * been removed by the user and as such no mapping could be made. */
126 builder += this->strings[0];
127 return;
128 }
129
130 builder.PutUtf8(this->type);
131
132 if (this->type == SCC_SWITCH_CASE) {
133 /*
134 * Format for case switch:
135 * <NUM CASES> <CASE1> <LEN1> <STRING1> <CASE2> <LEN2> <STRING2> <CASE3> <LEN3> <STRING3> <LENDEFAULT> <STRINGDEFAULT>
136 * Each LEN is printed using 2 bytes in big endian order.
137 */
138
139 /* "<NUM CASES>" */
140 int count = 0;
141 for (uint8_t i = 0; i < _current_language->num_cases; i++) {
142 /* Count the ones we have a mapped string for. */
143 if (this->strings.find(lm->GetReverseMapping(i, false)) != this->strings.end()) count++;
144 }
145 builder.PutUint8(count);
146
147 auto add_case = [&](std::string_view str) {
148 /* "<LENn>" */
149 uint16_t len = ClampTo<uint16_t>(str.size());
150 builder.PutUint16LE(len);
151
152 /* "<STRINGn>" */
153 builder += str.substr(0, len);
154 };
155
156 for (uint8_t i = 0; i < _current_language->num_cases; i++) {
157 /* Resolve the string we're looking for. */
158 int idx = lm->GetReverseMapping(i, false);
159 if (this->strings.find(idx) == this->strings.end()) continue;
160 auto &str = this->strings[idx];
161
162 /* "<CASEn>" */
163 builder.PutUint8(i + 1);
164
165 add_case(str);
166 }
167
168 /* "<STRINGDEFAULT>" */
169 add_case(this->strings[0]);
170 } else {
171 if (this->type == SCC_PLURAL_LIST) {
172 builder.PutUint8(lm->plural_form);
173 }
174
175 /*
176 * Format for choice list:
177 * <OFFSET> <NUM CHOICES> <LENs> <STRINGs>
178 */
179
180 /* "<OFFSET>" */
181 builder.PutUint8(this->offset - 0x80);
182
183 /* "<NUM CHOICES>" */
184 int count = (this->type == SCC_GENDER_LIST ? _current_language->num_genders : LANGUAGE_MAX_PLURAL_FORMS);
185 builder.PutUint8(count);
186
187 /* "<LENs>" */
188 for (int i = 0; i < count; i++) {
189 int idx = (this->type == SCC_GENDER_LIST ? lm->GetReverseMapping(i, true) : i + 1);
190 const auto &str = this->strings[this->strings.find(idx) != this->strings.end() ? idx : 0];
191 size_t len = str.size();
192 if (len > UINT8_MAX) GrfMsg(1, "choice list string is too long");
193 builder.PutUint8(ClampTo<uint8_t>(len));
194 }
195
196 /* "<STRINGs>" */
197 for (int i = 0; i < count; i++) {
198 int idx = (this->type == SCC_GENDER_LIST ? lm->GetReverseMapping(i, true) : i + 1);
199 const auto &str = this->strings[this->strings.find(idx) != this->strings.end() ? idx : 0];
200 uint8_t len = ClampTo<uint8_t>(str.size());
201 builder += str.substr(0, len);
202 }
203 }
204 }
205};
206
216std::string TranslateTTDPatchCodes(GrfID grfid, GRFLanguage language_id, bool allow_newlines, std::string_view str, StringControlCode byte80)
217{
218 /* Empty input string? Nothing to do here. */
219 if (str.empty()) return {};
220
221 StringConsumer consumer(str);
222
223 /* Is this an unicode string? */
224 bool unicode = consumer.ReadUtf8If(NFO_UTF8_IDENTIFIER);
225
226 /* Helper variable for a possible (string) mapping of plural/gender and cases. */
227 std::optional<UnmappedChoiceList> mapping_pg, mapping_c;
228 std::optional<std::reference_wrapper<std::string>> dest_c;
229
230 std::string dest;
231 StringBuilder builder(dest);
232 while (consumer.AnyBytesLeft()) {
233 char32_t c;
234 if (auto u = unicode ? consumer.TryReadUtf8() : std::nullopt; u.has_value()) {
235 c = *u;
236 /* 'Magic' range of control codes. */
237 if (0xE000 <= c && c <= 0xE0FF) {
238 c -= 0xE000;
239 } else if (c >= 0x20) {
240 if (!IsValidChar(c, CS_ALPHANUMERAL)) c = '?';
241 builder.PutUtf8(c);
242 continue;
243 }
244 } else {
245 c = consumer.ReadUint8(); // read as unsigned, otherwise integer promotion breaks it
246 }
247 assert(c <= 0xFF);
248 if (c == '\0') break;
249
250 switch (c) {
251 case 0x01:
252 consumer.SkipUint8();
253 builder.PutChar(' ');
254 break;
255 case 0x0A: break;
256 case 0x0D:
257 if (allow_newlines) {
258 builder.PutChar(0x0A);
259 } else {
260 GrfMsg(1, "Detected newline in string that does not allow one");
261 }
262 break;
263 case 0x0E: builder.PutUtf8(SCC_TINYFONT); break;
264 case 0x0F: builder.PutUtf8(SCC_BIGFONT); break;
265 case 0x1F:
266 consumer.SkipUint8();
267 consumer.SkipUint8();
268 builder.PutChar(' ');
269 break;
270 case 0x7B:
271 case 0x7C:
272 case 0x7D:
273 case 0x7E:
274 case 0x7F: builder.PutUtf8(SCC_NEWGRF_PRINT_DWORD_SIGNED + c - 0x7B); break;
275 case 0x80: builder.PutUtf8(byte80); break;
276 case 0x81:
277 {
278 uint16_t string = consumer.ReadUint16LE();
279 builder.PutUtf8(SCC_NEWGRF_STRINL);
280 builder.PutUtf8(MapGRFStringID(grfid, GRFStringID{string}));
281 break;
282 }
283 case 0x82:
284 case 0x83:
285 case 0x84: builder.PutUtf8(SCC_NEWGRF_PRINT_WORD_DATE_LONG + c - 0x82); break;
286 case 0x85: builder.PutUtf8(SCC_NEWGRF_DISCARD_WORD); break;
287 case 0x86: builder.PutUtf8(SCC_NEWGRF_ROTATE_TOP_4_WORDS); break;
288 case 0x87: builder.PutUtf8(SCC_NEWGRF_PRINT_WORD_VOLUME_LONG); break;
289 case 0x88: builder.PutUtf8(SCC_BLUE); break;
290 case 0x89: builder.PutUtf8(SCC_SILVER); break;
291 case 0x8A: builder.PutUtf8(SCC_GOLD); break;
292 case 0x8B: builder.PutUtf8(SCC_RED); break;
293 case 0x8C: builder.PutUtf8(SCC_PURPLE); break;
294 case 0x8D: builder.PutUtf8(SCC_LTBROWN); break;
295 case 0x8E: builder.PutUtf8(SCC_ORANGE); break;
296 case 0x8F: builder.PutUtf8(SCC_GREEN); break;
297 case 0x90: builder.PutUtf8(SCC_YELLOW); break;
298 case 0x91: builder.PutUtf8(SCC_DKGREEN); break;
299 case 0x92: builder.PutUtf8(SCC_CREAM); break;
300 case 0x93: builder.PutUtf8(SCC_BROWN); break;
301 case 0x94: builder.PutUtf8(SCC_WHITE); break;
302 case 0x95: builder.PutUtf8(SCC_LTBLUE); break;
303 case 0x96: builder.PutUtf8(SCC_GRAY); break;
304 case 0x97: builder.PutUtf8(SCC_DKBLUE); break;
305 case 0x98: builder.PutUtf8(SCC_BLACK); break;
306 case 0x9A:
307 {
308 uint8_t code = consumer.ReadUint8();
309 switch (code) {
310 case 0x00: goto string_end;
311 case 0x01: builder.PutUtf8(SCC_NEWGRF_PRINT_QWORD_CURRENCY); break;
312 /* 0x02: ignore next colour byte is not supported. It works on the final
313 * string and as such hooks into the string drawing routine. At that
314 * point many things already happened, such as splitting up of strings
315 * when drawn over multiple lines or right-to-left translations, which
316 * make the behaviour peculiar, e.g. only happening at specific width
317 * of windows. Or we need to add another pass over the string to just
318 * support this. As such it is not implemented in OpenTTD. */
319 case 0x03:
320 {
321 uint16_t tmp = consumer.ReadUint16LE();
323 builder.PutUtf8(tmp);
324 break;
325 }
326 case 0x06: builder.PutUtf8(SCC_NEWGRF_PRINT_BYTE_HEX); break;
327 case 0x07: builder.PutUtf8(SCC_NEWGRF_PRINT_WORD_HEX); break;
328 case 0x08: builder.PutUtf8(SCC_NEWGRF_PRINT_DWORD_HEX); break;
329 /* 0x09, 0x0A are TTDPatch internal use only string codes. */
330 case 0x0B: builder.PutUtf8(SCC_NEWGRF_PRINT_QWORD_HEX); break;
331 case 0x0C: builder.PutUtf8(SCC_NEWGRF_PRINT_WORD_STATION_NAME); break;
332 case 0x0D: builder.PutUtf8(SCC_NEWGRF_PRINT_WORD_WEIGHT_LONG); break;
333 case 0x0E:
334 case 0x0F:
335 {
336 const LanguageMap *lm = LanguageMap::GetLanguageMap(grfid, language_id);
337 int index = consumer.ReadUint8();
338 int mapped = lm != nullptr ? lm->GetMapping(index, code == 0x0E) : -1;
339 if (mapped >= 0) {
340 builder.PutUtf8(code == 0x0E ? SCC_GENDER_INDEX : SCC_SET_CASE);
341 builder.PutUtf8(code == 0x0E ? mapped : mapped + 1);
342 }
343 break;
344 }
345
346 case 0x10:
347 case 0x11:
348 if (!mapping_pg.has_value() && !mapping_c.has_value()) {
349 if (code == 0x10) consumer.SkipUint8(); // Skip the index
350 GrfMsg(1, "choice list {} marker found when not expected", code == 0x10 ? "next" : "default");
351 break;
352 } else {
353 auto &mapping = mapping_pg ? mapping_pg : mapping_c;
354 int index = (code == 0x10 ? consumer.ReadUint8() : 0);
355 if (mapping->strings.find(index) != mapping->strings.end()) {
356 GrfMsg(1, "duplicate choice list string, ignoring");
357 } else {
358 builder = StringBuilder(mapping->strings[index]);
359 if (!mapping_pg) dest_c = mapping->strings[index];
360 }
361 }
362 break;
363
364 case 0x12:
365 if (!mapping_pg.has_value() && !mapping_c.has_value()) {
366 GrfMsg(1, "choice list end marker found when not expected");
367 } else {
368 auto &mapping = mapping_pg ? mapping_pg : mapping_c;
369 auto &new_dest = mapping_pg && dest_c ? dest_c->get() : dest;
370 /* Now we can start flushing everything and clean everything up. */
371 mapping->Flush(LanguageMap::GetLanguageMap(grfid, language_id), new_dest);
372 if (!mapping_pg) dest_c.reset();
373 mapping.reset();
374
375 builder = StringBuilder(new_dest);
376 }
377 break;
378
379 case 0x13:
380 case 0x14:
381 case 0x15: {
382 auto &mapping = code == 0x14 ? mapping_c : mapping_pg;
383 /* Case mapping can have nested plural/gender mapping. Otherwise nesting is invalid. */
384 if (mapping.has_value() || mapping_pg.has_value()) {
385 GrfMsg(1, "choice lists can't be stacked, it's going to get messy now...");
386 if (code != 0x14) consumer.SkipUint8();
387 } else {
388 static const StringControlCode mp[] = { SCC_GENDER_LIST, SCC_SWITCH_CASE, SCC_PLURAL_LIST };
389 mapping.emplace(mp[code - 0x13], code == 0x14 ? 0 : consumer.ReadUint8());
390 }
391 break;
392 }
393
394 case 0x16:
395 case 0x17:
396 case 0x18:
397 case 0x19:
398 case 0x1A:
399 case 0x1B:
400 case 0x1C:
401 case 0x1D:
402 case 0x1E:
403 builder.PutUtf8(SCC_NEWGRF_PRINT_DWORD_DATE_LONG + code - 0x16);
404 break;
405
406 case 0x1F: builder.PutUtf8(SCC_PUSH_COLOUR); break;
407 case 0x20: builder.PutUtf8(SCC_POP_COLOUR); break;
408
409 case 0x21: builder.PutUtf8(SCC_NEWGRF_PRINT_DWORD_FORCE); break;
410
411 default:
412 GrfMsg(1, "missing handler for extended format code");
413 break;
414 }
415 break;
416 }
417
418 case 0x9B: builder.PutUtf8(SCC_TOWN); break;
419 case 0x9C: builder.PutUtf8(SCC_CITY); break;
420 case 0x9E: builder.PutUtf8(0x20AC); break; // Euro
421 case 0x9F: builder.PutUtf8(0x0178); break; // Y with diaeresis
422 case 0xA0: builder.PutUtf8(SCC_UP_ARROW); break;
423 case 0xAA: builder.PutUtf8(SCC_DOWN_ARROW); break;
424 case 0xAC: builder.PutUtf8(SCC_CHECKMARK); break;
425 case 0xAD: builder.PutUtf8(SCC_CROSS); break;
426 case 0xAF: builder.PutUtf8(SCC_RIGHT_ARROW); break;
427 case 0xB4: builder.PutUtf8(SCC_TRAIN); break;
428 case 0xB5: builder.PutUtf8(SCC_LORRY); break;
429 case 0xB6: builder.PutUtf8(SCC_BUS); break;
430 case 0xB7: builder.PutUtf8(SCC_PLANE); break;
431 case 0xB8: builder.PutUtf8(SCC_SHIP); break;
432 case 0xB9: builder.PutUtf8(SCC_SUPERSCRIPT_M1); break;
433 case 0xBC: builder.PutUtf8(SCC_SMALL_UP_ARROW); break;
434 case 0xBD: builder.PutUtf8(SCC_SMALL_DOWN_ARROW); break;
435 default:
436 /* Validate any unhandled character */
437 if (!IsValidChar(c, CS_ALPHANUMERAL)) c = '?';
438 builder.PutUtf8(c);
439 break;
440 }
441 }
442
443string_end:
444 if (mapping_pg.has_value() || mapping_c.has_value()) {
445 GrfMsg(1, "choice list was incomplete, the whole list is ignored");
446 }
447
448 return dest;
449}
450
457static void AddGRFTextToList(GRFTextList &list, GRFLanguage langid, std::string_view text_to_add)
458{
459 /* Loop through all languages and see if we can replace a string */
460 for (auto &text : list) {
461 if (text.langid == langid) {
462 text.text = text_to_add;
463 return;
464 }
465 }
466
467 /* If a string wasn't replaced, then we must append the new string */
468 list.emplace_back(langid, std::string{text_to_add});
469}
470
480void AddGRFTextToList(GRFTextList &list, GRFLanguage langid, GrfID grfid, bool allow_newlines, std::string_view text_to_add)
481{
482 AddGRFTextToList(list, langid, TranslateTTDPatchCodes(grfid, langid, allow_newlines, text_to_add));
483}
484
494void AddGRFTextToList(GRFTextWrapper &list, GRFLanguage langid, GrfID grfid, bool allow_newlines, std::string_view text_to_add)
495{
496 if (list == nullptr) list = std::make_shared<GRFTextList>();
497 AddGRFTextToList(*list, langid, grfid, allow_newlines, text_to_add);
498}
499
506void AddGRFTextToList(GRFTextWrapper &list, std::string_view text_to_add)
507{
508 if (list == nullptr) list = std::make_shared<GRFTextList>();
509 AddGRFTextToList(*list, GRFLanguage::Unspecified, text_to_add);
510}
511
522static StringID AddGRFString(GrfID grfid, GRFStringID stringid, GRFLanguage langid_to_add, bool allow_newlines, std::string_view text_to_add, StringID def_string)
523{
524 auto it = std::ranges::find_if(_grf_text, [&grfid, &stringid](const GRFTextEntry &grf_text) { return grf_text.grfid == grfid && grf_text.stringid == stringid; });
525 if (it == std::end(_grf_text)) {
526 /* Too many strings allocated, return empty. */
527 if (_grf_text.size() == TAB_SIZE_NEWGRF) return STR_EMPTY;
528
529 /* We didn't find our stringid and grfid in the list, allocate a new id. */
530 it = _grf_text.emplace(std::end(_grf_text));
531 it->grfid = grfid;
532 it->stringid = stringid;
533 it->def_string = def_string;
534 }
535 StringIndexInTab id(it - std::begin(_grf_text));
536
537 std::string newtext = TranslateTTDPatchCodes(grfid, langid_to_add, allow_newlines, text_to_add);
538 AddGRFTextToList(it->textholder, langid_to_add, newtext);
539
540 GrfMsg(3, "Added 0x{:X} grfid {:08X} string 0x{:X} lang 0x{:X} string '{}' ({:X})", id, std::byteswap(grfid), stringid, langid_to_add, newtext, MakeStringID(TEXT_TAB_NEWGRF_START, id));
541
543}
544
556StringID AddGRFString(GrfID grfid, GRFStringID stringid, uint8_t langid_to_add, bool new_scheme, bool allow_newlines, std::string_view text_to_add, StringID def_string)
557{
558 if (new_scheme) return AddGRFString(grfid, stringid, static_cast<GRFLanguage>(langid_to_add), allow_newlines, text_to_add, def_string);
559
560 OldGRFLanguages old_lang{langid_to_add};
561
562 /* When working with the old language scheme (grf_version is less than 7) and
563 * English or American is among the set bits, simply add it as English in
564 * the new scheme, i.e. as langid = 1.
565 * If English is set, it is pretty safe to assume the translations are not
566 * actually translated.
567 */
568 if (old_lang.Any({GRFLanguage::American, GRFLanguage::English})) return AddGRFString(grfid, stringid, GRFLanguage::English, allow_newlines, text_to_add, def_string);
569
570 StringID ret = STR_EMPTY;
572 if (old_lang.Test(lang)) ret = AddGRFString(grfid, stringid, lang, allow_newlines, text_to_add, def_string);
573 }
574 return ret;
575}
576
584{
585 auto it = std::ranges::find_if(_grf_text, [&grfid, &stringid](const GRFTextEntry &grf_text) { return grf_text.grfid == grfid && grf_text.stringid == stringid; });
586 if (it != std::end(_grf_text)) {
587 StringIndexInTab id(it - std::begin(_grf_text));
589 }
590
591 return STR_UNDEFINED;
592}
593
594
603std::optional<std::string_view> GetGRFStringFromGRFText(const GRFTextList &text_list)
604{
605 std::optional<std::string_view> default_text;
606
607 /* Search the list of lang-strings of this stringid for current lang */
608 for (const auto &text : text_list) {
609 if (text.langid == _current_language->newgrflangid) return text.text;
610
611 /* If the current string is English or American, set it as the
612 * fallback language if the specific language isn't available. */
613 if (text.langid == GRFLanguage::Unspecified || (!default_text.has_value() && (text.langid == GRFLanguage::English || text.langid == GRFLanguage::American))) {
614 default_text = text.text;
615 }
616 }
617
618 return default_text;
619}
620
629std::optional<std::string_view> GetGRFStringFromGRFText(const GRFTextWrapper &text)
630{
631 return text ? GetGRFStringFromGRFText(*text) : std::nullopt;
632}
633
639std::string_view GetGRFStringPtr(StringIndexInTab stringid)
640{
641 assert(stringid.base() < _grf_text.size());
642 assert(_grf_text[stringid].grfid != 0);
643
644 auto str = GetGRFStringFromGRFText(_grf_text[stringid].textholder);
645 if (str.has_value()) return *str;
646
647 /* Use the default string ID if the fallback string isn't available */
648 return GetStringPtr(_grf_text[stringid].def_string);
649}
650
651bool CheckGrfLangID(uint8_t lang_id, uint8_t grf_version)
652{
653 if (grf_version < 7) {
654 OldGRFLanguages old_lang{lang_id};
655 switch (_current_language->newgrflangid) {
656 case GRFLanguage::German: return old_lang.Test(GRFLanguage::German);
657 case GRFLanguage::French: return old_lang.Test(GRFLanguage::French);
658 case GRFLanguage::Spanish: return old_lang.Test(GRFLanguage::Spanish);
659 default: return old_lang.Any({GRFLanguage::American, GRFLanguage::English});
660 }
661 }
662
663 GRFLanguage new_lang = static_cast<GRFLanguage>(lang_id);
664 return new_lang == _current_language->newgrflangid || new_lang == GRFLanguage::Unspecified;
665}
666
672{
673 _grf_text.clear();
674}
675
676struct TextRefStack {
677private:
678 std::vector<uint8_t> stack;
679public:
680 const GRFFile *grffile = nullptr;
681
682 TextRefStack(const GRFFile *grffile, std::span<const int32_t> textstack) : grffile(grffile)
683 {
684 this->stack.reserve(textstack.size() * 4 + 6); // for translations it is reasonable to push 3 word and rotate them
685 for (int32_t value : std::ranges::reverse_view(textstack)) {
686 this->PushDWord(value);
687 }
688 }
689
690 uint8_t PopUnsignedByte()
691 {
692 if (this->stack.empty()) return 0;
693 auto res = this->stack.back();
694 this->stack.pop_back();
695 return res;
696 }
697 int8_t PopSignedByte() { return (int8_t)this->PopUnsignedByte(); }
698
699 uint16_t PopUnsignedWord()
700 {
701 uint16_t val = this->PopUnsignedByte();
702 return val | (this->PopUnsignedByte() << 8);
703 }
704 int16_t PopSignedWord() { return (int32_t)this->PopUnsignedWord(); }
705
706 uint32_t PopUnsignedDWord()
707 {
708 uint32_t val = this->PopUnsignedWord();
709 return val | (this->PopUnsignedWord() << 16);
710 }
711 int32_t PopSignedDWord() { return (int32_t)this->PopUnsignedDWord(); }
712
713 uint64_t PopUnsignedQWord()
714 {
715 uint64_t val = this->PopUnsignedDWord();
716 return val | (((uint64_t)this->PopUnsignedDWord()) << 32);
717 }
718 int64_t PopSignedQWord() { return (int64_t)this->PopUnsignedQWord(); }
719
722 {
723 auto w1 = this->PopUnsignedWord();
724 auto w2 = this->PopUnsignedWord();
725 auto w3 = this->PopUnsignedWord();
726 auto w4 = this->PopUnsignedWord();
727 this->PushWord(w3);
728 this->PushWord(w2);
729 this->PushWord(w1);
730 this->PushWord(w4);
731 }
732
733 void PushByte(uint8_t b)
734 {
735 this->stack.push_back(b);
736 }
737
738 void PushWord(uint16_t w)
739 {
740 this->PushByte(GB(w, 8, 8));
741 this->PushByte(GB(w, 0, 8));
742 }
743
744 void PushDWord(uint32_t dw)
745 {
746 this->PushWord(GB(dw, 16, 16));
747 this->PushWord(GB(dw, 0, 16));
748 }
749};
750
751static void HandleNewGRFStringControlCodes(std::string_view str, TextRefStack &stack, std::vector<StringParameter> &params);
752
760static void ProcessNewGRFStringControlCode(char32_t scc, StringConsumer &consumer, TextRefStack &stack, std::vector<StringParameter> &params)
761{
762 /* There is data on the NewGRF text stack, and we want to move them to OpenTTD's string stack.
763 * After this call, a new call is made with `modify_parameters` set to false when the string is finally formatted. */
764 switch (scc) {
765 default: return;
766
767 case SCC_PLURAL_LIST:
768 consumer.SkipUint8(); // plural form
769 [[fallthrough]];
770 case SCC_GENDER_LIST: {
771 consumer.SkipUint8(); // offset
772 /* plural and gender choices cannot contain any string commands, so just skip the whole thing */
773 uint num = consumer.ReadUint8();
774 uint total_len = 0;
775 for (uint i = 0; i != num; i++) {
776 total_len += consumer.ReadUint8();
777 }
778 consumer.Skip(total_len);
779 break;
780 }
781
782 case SCC_SWITCH_CASE: {
783 /* skip all cases and continue with default case */
784 uint num = consumer.ReadUint8();
785 for (uint i = 0; i != num; i++) {
786 consumer.SkipUint8();
787 auto len = consumer.ReadUint16LE();
788 consumer.Skip(len);
789 }
790 consumer.SkipUint16LE(); // length of default
791 break;
792 }
793
794 case SCC_GENDER_INDEX:
795 case SCC_SET_CASE:
796 consumer.SkipUint8();
797 break;
798
799 case SCC_ARG_INDEX:
800 NOT_REACHED();
801 break;
802
803 case SCC_NEWGRF_PRINT_BYTE_SIGNED: params.emplace_back(stack.PopSignedByte()); break;
804 case SCC_NEWGRF_PRINT_QWORD_CURRENCY: params.emplace_back(stack.PopSignedQWord()); break;
805
807 case SCC_NEWGRF_PRINT_DWORD_SIGNED: params.emplace_back(stack.PopSignedDWord()); break;
808
809 case SCC_NEWGRF_PRINT_BYTE_HEX: params.emplace_back(stack.PopUnsignedByte()); break;
810 case SCC_NEWGRF_PRINT_QWORD_HEX: params.emplace_back(stack.PopUnsignedQWord()); break;
811
815 case SCC_NEWGRF_PRINT_WORD_SIGNED: params.emplace_back(stack.PopSignedWord()); break;
816
822 case SCC_NEWGRF_PRINT_WORD_UNSIGNED: params.emplace_back(stack.PopUnsignedWord()); break;
823
827 case SCC_NEWGRF_PRINT_DWORD_HEX: params.emplace_back(stack.PopUnsignedDWord()); break;
828
829 /* Dates from NewGRFs have 1920-01-01 as their zero point, convert it to OpenTTD's epoch. */
831 case SCC_NEWGRF_PRINT_WORD_DATE_SHORT: params.emplace_back(CalendarTime::DAYS_TILL_ORIGINAL_BASE_YEAR + stack.PopUnsignedWord()); break;
832
833 case SCC_NEWGRF_DISCARD_WORD: stack.PopUnsignedWord(); break;
834
836 case SCC_NEWGRF_PUSH_WORD: stack.PushWord(consumer.ReadUtf8(0)); break;
837
841 params.emplace_back(GetCargoTranslation(stack.PopUnsignedWord(), stack.grffile));
842 params.emplace_back(stack.PopUnsignedWord());
843 break;
844
845 case SCC_NEWGRF_STRINL: {
846 StringID stringid = consumer.ReadUtf8(STR_NULL);
847 /* We also need to handle the substring's stack usage. */
848 HandleNewGRFStringControlCodes(GetStringPtr(stringid), stack, params);
849 break;
850 }
851
853 StringID stringid = MapGRFStringID(stack.grffile->grfid, GRFStringID{stack.PopUnsignedWord()});
854 params.emplace_back(stringid);
855 /* We also need to handle the substring's stack usage. */
856 HandleNewGRFStringControlCodes(GetStringPtr(stringid), stack, params);
857 break;
858 }
859
861 CargoType cargo = GetCargoTranslation(stack.PopUnsignedWord(), stack.grffile);
862 params.emplace_back(cargo < NUM_CARGO ? 1ULL << cargo : 0);
863 break;
864 }
865 }
866}
867
874char32_t RemapNewGRFStringControlCode(char32_t scc, StringConsumer &consumer)
875{
876 switch (scc) {
877 default:
878 return scc;
879
884 return SCC_COMMA;
885
890 return SCC_HEX;
891
894 return SCC_CURRENCY_LONG;
895
898 return SCC_DATE_LONG;
899
902 return SCC_DATE_SHORT;
903
905 return SCC_VELOCITY;
906
908 return SCC_VOLUME_LONG;
909
911 return SCC_VOLUME_SHORT;
912
914 return SCC_WEIGHT_LONG;
915
917 return SCC_WEIGHT_SHORT;
918
920 return SCC_POWER;
921
923 return SCC_FORCE;
924
926 return SCC_CARGO_LONG;
927
929 return SCC_CARGO_SHORT;
930
932 return SCC_CARGO_TINY;
933
935 return SCC_CARGO_LIST;
936
938 return SCC_STATION_NAME;
939
940 /* These NewGRF string codes modify the NewGRF stack or otherwise do not map to OpenTTD string codes. */
942 consumer.SkipUtf8();
943 return 0;
944
947 return 0;
948 }
949}
950
957static void HandleNewGRFStringControlCodes(std::string_view str, TextRefStack &stack, std::vector<StringParameter> &params)
958{
959 StringConsumer consumer(str);
960 while (consumer.AnyBytesLeft()) {
961 char32_t scc = consumer.ReadUtf8();
962 ProcessNewGRFStringControlCode(scc, consumer, stack, params);
963 }
964}
965
973std::vector<StringParameter> GetGRFStringTextStackParameters(const GRFFile *grffile, StringID stringid, std::span<const int32_t> textstack)
974{
975 if (stringid == INVALID_STRING_ID) return {};
976
977 auto str = GetStringPtr(stringid);
978
979 std::vector<StringParameter> params;
980 params.reserve(20);
981
982 TextRefStack stack{grffile, textstack};
983 HandleNewGRFStringControlCodes(str, stack, params);
984
985 return params;
986}
987
995std::string GetGRFStringWithTextStack(const struct GRFFile *grffile, GRFStringID grfstringid, std::span<const int32_t> textstack)
996{
997 StringID stringid = GetGRFStringID(grffile->grfid, grfstringid);
998 auto params = GetGRFStringTextStackParameters(grffile, stringid, textstack);
999 return GetStringWithArgs(stringid, params);
1000}
static constexpr uint GB(const T x, const uint8_t s, const uint8_t n)
Fetch n bits from x, started at bit s.
constexpr enable_if_t< is_integral_v< T >, T > byteswap(T x) noexcept
Custom implementation of std::byteswap; remove once we build with C++23.
static constexpr CargoType NUM_CARGO
Maximum number of cargo types in a game.
Definition cargo_type.h:75
CargoType
Cargo slots to indicate a cargo type within a game.
Definition cargo_type.h:22
constexpr bool Test(Tvalue_type value) const
Test if the value-th bit is set.
constexpr bool Any(const Timpl &other) const
Test if any of the given values are set.
void PutUtf8(char32_t c)
Append UTF-8 char.
void PutUint16LE(uint16_t value)
Append binary uint16 using little endian.
void PutChar(char c)
Append 8-bit char.
void PutUint8(uint8_t value)
Append binary uint8.
Enum-as-bit-set wrapper.
Compose data into a growing std::string.
Parse data from a string / buffer.
char32_t ReadUtf8(char32_t def='?')
Read UTF-8 character, and advance reader.
void SkipUtf8()
Skip UTF-8 character, and advance reader.
bool AnyBytesLeft() const noexcept
Check whether any bytes left to read.
uint8_t ReadUint8(uint8_t def=0)
Read binary uint8, and advance reader.
void SkipUint16LE()
Skip binary uint16, and advance reader.
uint16_t ReadUint16LE(uint16_t def=0)
Read binary uint16 using little endian, and advance reader.
void SkipUint8()
Skip binary uint8.
std::optional< char32_t > TryReadUtf8()
Try to read a UTF-8 character, and then advance reader.
bool ReadUtf8If(char32_t c)
Check whether the next UTF-8 char matches 'c', and skip it.
void Skip(size_type len)
Discard some bytes.
static constexpr TimerGame< struct Calendar >::Date DAYS_TILL_ORIGINAL_BASE_YEAR
A sort-of mixin that implements 'at(pos)' and 'operator[](pos)' only for a specific type.
Control codes that are embedded in the translation strings.
StringControlCode
List of string control codes used for string formatting, displaying, and by strgen to generate the la...
@ SCC_NEWGRF_PRINT_WORD_VOLUME_SHORT
9A 19: Read 2 bytes from the stack as short signed volume
@ SCC_NEWGRF_PRINT_DWORD_CURRENCY
8F: Read 4 bytes from the stack as currency
@ SCC_NEWGRF_PRINT_WORD_HEX
9A 07: Read 2 bytes from the stack and print it as hex
@ SCC_NEWGRF_PRINT_WORD_WEIGHT_SHORT
9A 1A: Read 2 bytes from the stack as short unsigned weight
@ SCC_NEWGRF_ROTATE_TOP_4_WORDS
86: Rotate the top 4 words of the stack (W4 W1 W2 W3)
@ SCC_TINYFONT
Switch to small font.
@ SCC_NEWGRF_PRINT_DWORD_DATE_LONG
9A 16: Read 4 bytes from the stack as base 0 date
@ SCC_NEWGRF_PRINT_DWORD_HEX
9A 08: Read 4 bytes from the stack and print it as hex
@ SCC_NEWGRF_PRINT_WORD_DATE_SHORT
83: Read 2 bytes from the stack as base 1920 date
@ SCC_BIGFONT
Switch to large font.
@ SCC_NEWGRF_PRINT_BYTE_HEX
9A 06: Read 1 byte from the stack and print it as hex
@ SCC_NEWGRF_PRINT_WORD_UNSIGNED
7E: Read 2 bytes from the stack as unsigned value
@ SCC_NEWGRF_PRINT_BYTE_SIGNED
7D: Read 1 byte from the stack as signed value
@ SCC_NEWGRF_PRINT_WORD_CARGO_NAME
9A 1E: Read 2 bytes from the stack as cargo name
@ SCC_NEWGRF_PRINT_DWORD_SIGNED
7B: Read 4 bytes from the stack
@ SCC_NEWGRF_STRINL
Inline another string at the current position, StringID is encoded in the string.
@ SCC_NEWGRF_PRINT_WORD_CARGO_SHORT
9A 1C: Read 2 + 2 bytes from the stack as cargo type (translated) and unsigned cargo amount
@ SCC_NEWGRF_PRINT_WORD_STATION_NAME
9A 0C: Read 2 bytes from the stack as station name
@ SCC_NEWGRF_PRINT_WORD_DATE_LONG
82: Read 2 bytes from the stack as base 1920 date
@ SCC_NEWGRF_PRINT_WORD_SIGNED
7C: Read 2 bytes from the stack as signed value
@ SCC_NEWGRF_PRINT_WORD_POWER
9A 18: Read 2 bytes from the stack as unsigned power
@ SCC_NEWGRF_PUSH_WORD
9A 03: Pushes 2 bytes onto the stack
@ SCC_NEWGRF_PRINT_WORD_STRING_ID
81: Read 2 bytes from the stack as String ID
@ SCC_NEWGRF_PRINT_WORD_VOLUME_LONG
87: Read 2 bytes from the stack as long signed volume
@ SCC_NEWGRF_DISCARD_WORD
85: Discard the next two bytes
@ SCC_NEWGRF_PRINT_DWORD_FORCE
9A 21: Read 4 bytes from the stack as unsigned force
@ SCC_NEWGRF_PRINT_WORD_CARGO_TINY
9A 1D: Read 2 + 2 bytes from the stack as cargo type (translated) and unsigned cargo amount
@ SCC_NEWGRF_PRINT_QWORD_HEX
9A 0B: Read 8 bytes from the stack and print it as hex
@ SCC_NEWGRF_PRINT_WORD_CARGO_LONG
9A 1B: Read 2 + 2 bytes from the stack as cargo type (translated) and unsigned cargo amount
@ SCC_NEWGRF_PRINT_WORD_SPEED
84: Read 2 bytes from the stack as signed speed
@ SCC_NEWGRF_PRINT_DWORD_DATE_SHORT
9A 17: Read 4 bytes from the stack as base 0 date
@ SCC_NEWGRF_PRINT_WORD_WEIGHT_LONG
9A 0D: Read 2 bytes from the stack as long unsigned weight
@ SCC_NEWGRF_PRINT_QWORD_CURRENCY
9A 01: Read 8 bytes from the stack as currency
Functions related to debugging.
Information about languages and their files.
const LanguageMetadata * _current_language
The currently loaded language.
Definition strings.cpp:54
constexpr To ClampTo(From value)
Clamp the given value down to lie within the requested type.
Base for the NewGRF implementation.
StringID MapGRFStringID(GrfID grfid, GRFStringID str)
Used when setting an object's property to map to the GRF's strings while taking in consideration the ...
CargoType GetCargoTranslation(uint8_t cargo, const GRFFile *grffile, bool usebit)
Translate a GRF-local cargo slot/bitnum into a CargoType.
Cargo support for NewGRFs.
Functionality related to the temporary and persistent storage arrays for NewGRFs.
void CleanUpStrings()
House cleaning.
std::string_view GetGRFStringPtr(StringIndexInTab stringid)
Get a C-string from a stringid set by a newgrf.
static void HandleNewGRFStringControlCodes(std::string_view str, TextRefStack &stack, std::vector< StringParameter > &params)
Handle control codes in a NewGRF string, processing the stack and filling parameters.
EnumBitSet< GRFLanguage, uint8_t > OldGRFLanguages
The bitmask of the old GRF languages, which are conveniently the same as the first few GRFLanguages.
std::vector< StringParameter > GetGRFStringTextStackParameters(const GRFFile *grffile, StringID stringid, std::span< const int32_t > textstack)
Process the text ref stack for a GRF String and return its parameters.
std::string GetGRFStringWithTextStack(const struct GRFFile *grffile, GRFStringID grfstringid, std::span< const int32_t > textstack)
Format a GRF string using the text ref stack for parameters.
std::string TranslateTTDPatchCodes(GrfID grfid, GRFLanguage language_id, bool allow_newlines, std::string_view str, StringControlCode byte80)
Translate TTDPatch string codes into something OpenTTD can handle (better).
std::optional< std::string_view > GetGRFStringFromGRFText(const GRFTextList &text_list)
Get a C-string from a GRFText-list.
StringID GetGRFStringID(GrfID grfid, GRFStringID stringid)
Returns the index for this stringid associated with its grfID.
static void AddGRFTextToList(GRFTextList &list, GRFLanguage langid, std::string_view text_to_add)
Add a new text to a GRFText list.
static void ProcessNewGRFStringControlCode(char32_t scc, StringConsumer &consumer, TextRefStack &stack, std::vector< StringParameter > &params)
Process NewGRF string control code instructions.
char32_t RemapNewGRFStringControlCode(char32_t scc, StringConsumer &consumer)
Emit OpenTTD's internal string code for the different NewGRF string codes.
static StringID AddGRFString(GrfID grfid, GRFStringID stringid, GRFLanguage langid_to_add, bool allow_newlines, std::string_view text_to_add, StringID def_string)
Add the new read string into our structure.
Header of Action 04 "universal holder" structure and functions.
GRFLanguage
Incomplete enumeration of the (New)GRF languages.
@ Unspecified
No specific language.
@ English
English (English UK).
@ American
American (English US).
@ Spanish
Spanish.
StrongType::Typedef< uint32_t, struct GRFStringIDTag, StrongType::Compare, StrongType::Integer > GRFStringID
Type for GRF-internal string IDs.
std::shared_ptr< GRFTextList > GRFTextWrapper
Reference counted wrapper around a GRFText pointer.
std::vector< GRFText > GRFTextList
A GRF text with a list of translations.
static const char32_t NFO_UTF8_IDENTIFIER
This character (thorn) indicates a unicode string to NFO.
uint32_t GrfID
The unique identifier of a NewGRF.
Definition newgrf_type.h:15
A number of safeguards to prevent using unsafe methods.
Definition of base types and functions in a cross-platform compatible way.
bool IsValidChar(char32_t key, CharSetFilter afilter)
Only allow certain keys.
Definition string.cpp:375
Compose strings from textual and binary data.
Parse strings.
Functions related to low-level strings.
@ CS_ALPHANUMERAL
Both numeric and alphabetic and spaces and stuff.
Definition string_type.h:25
void GetStringWithArgs(StringBuilder &builder, StringID string, StringParameters &args, uint case_index, bool game_script)
Get a parsed string with most special stringcodes replaced by the string parameters.
Definition strings.cpp:336
StringID MakeStringID(StringTab tab, StringIndexInTab index)
Create a StringID.
Types and functions related to the internal workings of formatting OpenTTD's strings.
uint32_t StringID
Numeric value that represents a string, independent of the selected language.
StrongType::Typedef< uint32_t, struct StringIndexInTabTag, StrongType::Compare, StrongType::Integer > StringIndexInTab
The index/offset of a string within a StringTab.
static const StringID INVALID_STRING_ID
Constant representing an invalid string (16bit in case it is used in savegames).
static const uint TAB_SIZE_NEWGRF
Number of strings for NewGRFs.
@ TEXT_TAB_NEWGRF_START
Start of NewGRF supplied strings.
Dynamic data of a loaded NewGRF.
Definition newgrf.h:124
Holder of the above structure.
Mapping between NewGRF and OpenTTD IDs.
Mapping of language data between a NewGRF and OpenTTD.
std::vector< Mapping > gender_map
Mapping of NewGRF and OpenTTD IDs for genders.
int GetReverseMapping(int openttd_id, bool gender) const
Get the mapping from OpenTTD's internal ID to the NewGRF supplied ID.
int plural_form
The plural form used for this language.
static const LanguageMap * GetLanguageMap(GrfID grfid, GRFLanguage language_id)
Get the language map associated with a given NewGRF and language.
Definition newgrf.cpp:363
std::vector< Mapping > case_map
Mapping of NewGRF and OpenTTD IDs for cases.
int GetMapping(int newgrf_id, bool gender) const
Get the mapping from the NewGRF supplied ID to OpenTTD's internal ID.
void RotateTop4Words()
Rotate the top four words down: W1, W2, W3, W4 -> W4, W1, W2, W3.
StringControlCode type
The type of choice list.
int offset
The offset for the plural/gender form.
std::map< int, std::string > strings
Mapping of NewGRF supplied ID to the different strings in the choice list.
UnmappedChoiceList(StringControlCode type, int offset)
Initialise the mapping.
void Flush(const LanguageMap *lm, std::string &dest)
Flush this choice list into the destination string.
Definition of the game-calendar-timer.