11#include "../core/endian_func.hpp"
12#include "../core/mem_func.hpp"
13#include "../error_func.h"
14#include "../string_func.h"
15#include "../core/string_builder.hpp"
16#include "../table/control_codes.h"
20#include "../table/strgen_tables.h"
22#include "../safeguards.h"
26static const char *_cur_ident;
28static size_t _cur_argidx;
33 std::optional<size_t> argno;
34 std::optional<uint8_t> casei;
37static size_t TranslateArgumentIdx(
size_t arg,
size_t offset = 0);
45 caseidx(caseidx), string(string)
57 name(name), english(english), index(index), line(line)
95 this->
strings[ls->index] = std::move(ls);
108 return it->second.get();
120 hash = std::rotl(hash, 3) ^ c;
121 hash = (hash & 1 ? hash >> 1 ^ 0xDEADBEEF : hash >> 1);
138 hash ^= i * 0x717239;
139 hash = (hash & 1 ? hash >> 1 ^ 0xDEADBEEF : hash >> 1);
142 const char *s = ls->
english.c_str();
144 while ((cs = ParseCommandString(&s)).cmd !=
nullptr) {
147 hash ^= (cs.cmd - _cmd_structs) * 0x1234567;
148 hash = (hash & 1 ? hash >> 1 ^ 0xF00BAA4 : hash >> 1);
163 while (count > 0 && this->
strings[(tab *
TAB_SIZE) + count - 1] ==
nullptr) --count;
167static size_t Utf8Validate(
const char *s)
174 }
else if (
GB(s[0], 5, 3) == 6 && IsUtf8Part(s[1])) {
176 c =
GB(s[0], 0, 5) << 6 |
GB(s[1], 0, 6);
177 if (c >= 0x80)
return 2;
178 }
else if (
GB(s[0], 4, 4) == 14 && IsUtf8Part(s[1]) && IsUtf8Part(s[2])) {
180 c =
GB(s[0], 0, 4) << 12 |
GB(s[1], 0, 6) << 6 |
GB(s[2], 0, 6);
181 if (c >= 0x800)
return 3;
182 }
else if (
GB(s[0], 3, 5) == 30 && IsUtf8Part(s[1]) && IsUtf8Part(s[2]) && IsUtf8Part(s[3])) {
184 c =
GB(s[0], 0, 3) << 18 |
GB(s[1], 0, 6) << 12 |
GB(s[2], 0, 6) << 6 |
GB(s[3], 0, 6);
185 if (c >= 0x10000 && c <= 0x10FFFF)
return 4;
191void EmitSingleChar(
StringBuilder &builder,
const char *buf,
char32_t value)
193 if (*buf !=
'\0') StrgenWarning(
"Ignoring trailing letters in command");
199static std::pair<std::optional<size_t>, std::optional<size_t>> ParseRelNum(
const char **buf)
201 const char *s = *buf;
204 while (*s ==
' ' || *s ==
'\t') s++;
205 size_t v = std::strtoul(s, &end, 0);
206 if (end == s)
return {};
207 std::optional<size_t> offset;
211 offset = std::strtoul(s, &end, 0);
212 if (end == s)
return {};
219std::optional<std::string_view> ParseWord(
const char **buf)
221 const char *s = *buf;
223 while (*s ==
' ' || *s ==
'\t') s++;
224 if (*s ==
'\0')
return {};
227 const char *begin = ++s;
230 if (*s ==
'\0') StrgenFatal(
"Unterminated quotes");
233 return std::string_view(begin, s - begin);
239 const char *begin = s;
241 if (*s ==
'\0' || *s ==
' ' || *s ==
'\t') {
243 return std::string_view(begin, s - begin);
252static void EmitWordList(
StringBuilder &builder,
const std::vector<std::string> &words)
254 builder.
PutUint8(
static_cast<uint8_t
>(words.size()));
255 for (
size_t i = 0; i < words.size(); i++) {
256 size_t len = words[i].size();
257 if (len > UINT8_MAX) StrgenFatal(
"WordList {}/{} string '{}' too long, max bytes {}", i + 1, words.size(), words[i], UINT8_MAX);
258 builder.
PutUint8(
static_cast<uint8_t
>(len));
260 for (
size_t i = 0; i < words.size(); i++) {
261 builder.
Put(words[i]);
265void EmitPlural(
StringBuilder &builder,
const char *buf,
char32_t)
268 auto [argidx, offset] = ParseRelNum(&buf);
269 if (!argidx.has_value()) {
270 if (_cur_argidx == 0) StrgenFatal(
"Plural choice needs positional reference");
271 argidx = _cur_argidx - 1;
274 const CmdStruct *cmd = _cur_pcs.consuming_commands[*argidx];
275 if (!offset.has_value()) {
277 if (cmd ==
nullptr || !cmd->default_plural_offset.has_value()) {
278 StrgenFatal(
"Command '{}' has no (default) plural position", cmd ==
nullptr ?
"<empty>" : cmd->cmd);
280 offset = cmd->default_plural_offset;
284 std::vector<std::string> words;
286 auto word = ParseWord(&buf);
287 if (!word.has_value())
break;
288 words.emplace_back(*word);
292 StrgenFatal(
"{}: No plural words", _cur_ident);
296 if (expected != words.size()) {
298 StrgenFatal(
"{}: Invalid number of plural forms. Expecting {}, found {}.", _cur_ident,
299 expected, words.size());
301 if (_strgen.show_warnings) StrgenWarning(
"'{}' is untranslated. Tweaking english string to allow compilation for plural forms", _cur_ident);
302 if (words.size() > expected) {
303 words.resize(expected);
305 while (words.size() < expected) {
306 words.push_back(words.back());
312 builder.
PutUtf8(SCC_PLURAL_LIST);
314 builder.
PutUint8(
static_cast<uint8_t
>(TranslateArgumentIdx(*argidx, *offset)));
315 EmitWordList(builder, words);
318void EmitGender(
StringBuilder &builder,
const char *buf,
char32_t)
325 if (nw >=
MAX_NUM_GENDERS) StrgenFatal(
"G argument '{}' invalid", buf);
328 builder.
PutUtf8(SCC_GENDER_INDEX);
333 auto [argidx, offset] = ParseRelNum(&buf);
334 if (!argidx.has_value()) argidx = _cur_argidx;
335 if (!offset.has_value()) offset = 0;
337 const CmdStruct *cmd = _cur_pcs.consuming_commands[*argidx];
339 StrgenFatal(
"Command '{}' can't have a gender", cmd ==
nullptr ?
"<empty>" : cmd->cmd);
342 std::vector<std::string> words;
344 auto word = ParseWord(&buf);
345 if (!word.has_value())
break;
346 words.emplace_back(*word);
348 if (words.size() != _strgen.
lang.
num_genders) StrgenFatal(
"Bad # of arguments for gender command");
350 assert(
IsInsideBS(cmd->value, SCC_CONTROL_START, UINT8_MAX));
351 builder.
PutUtf8(SCC_GENDER_LIST);
352 builder.
PutUint8(
static_cast<uint8_t
>(TranslateArgumentIdx(*argidx, *offset)));
353 EmitWordList(builder, words);
357static const CmdStruct *FindCmd(std::string_view s)
359 for (
const auto &cs : _cmd_structs) {
360 if (cs.cmd == s)
return &cs;
365static uint8_t ResolveCaseName(std::string_view str)
368 if (case_idx >=
MAX_NUM_CASES) StrgenFatal(
"Invalid case-name '{}'", str);
376 const char *s = *str;
379 for (; *s !=
'{'; s++) {
380 if (*s ==
'\0')
return {};
384 if (*s >=
'0' && *s <=
'9') {
387 result.argno = std::strtoul(s, &end, 0);
388 if (*end !=
':') StrgenFatal(
"missing arg #");
393 const char *start = s;
397 }
while (c !=
'}' && c !=
' ' && c !=
'=' && c !=
'.' && c != 0);
399 std::string_view command(start, s - start - 1);
400 result.cmd = FindCmd(command);
401 if (result.cmd ==
nullptr) {
402 StrgenError(
"Undefined command '{}'", command);
407 const char *casep = s;
410 StrgenFatal(
"Command '{}' can't have a case", result.cmd->cmd);
415 }
while (c !=
'}' && c !=
' ' && c !=
'\0');
416 result.casei = ResolveCaseName(std::string_view(casep, s - casep - 1));
420 StrgenError(
"Missing }} from command '{}'", start);
432 StrgenError(
"Missing }} from command '{}'", start);
452 data(data), file(file), master(master), translation(translation)
463 auto cs = ParseCommandString(&s);
465 if (cs.cmd ==
nullptr)
break;
468 if (cs.argno.has_value() && cs.cmd->consumes == 0) StrgenFatal(
"Non consumer param can't have a paramindex");
470 if (cs.cmd->consumes > 0) {
471 if (cs.argno.has_value()) argidx = *cs.argno;
472 if (argidx >= p.consuming_commands.max_size()) StrgenFatal(
"invalid param idx {}", argidx);
473 if (p.consuming_commands[argidx] !=
nullptr && p.consuming_commands[argidx] != cs.cmd) StrgenFatal(
"duplicate param idx {}", argidx);
475 p.consuming_commands[argidx++] = cs.cmd;
477 p.non_consuming_commands.emplace_back(cs.cmd, std::move(cs.param));
486 if (a ==
nullptr)
return nullptr;
488 if (a->cmd ==
"STRING1" ||
489 a->cmd ==
"STRING2" ||
490 a->cmd ==
"STRING3" ||
491 a->cmd ==
"STRING4" ||
492 a->cmd ==
"STRING5" ||
493 a->cmd ==
"STRING6" ||
494 a->cmd ==
"STRING7" ||
495 a->cmd ==
"RAW_STRING") {
496 return FindCmd(
"STRING");
502static bool CheckCommandsMatch(
const char *a,
const char *b,
const char *name)
516 if (templ.non_consuming_commands.max_size() != lang.non_consuming_commands.max_size()) {
517 StrgenWarning(
"{}: template string and language string have a different # of commands", name);
521 for (
auto &templ_nc : templ.non_consuming_commands) {
524 for (
auto &lang_nc : lang.non_consuming_commands) {
525 if (templ_nc.cmd == lang_nc.cmd && templ_nc.param == lang_nc.param) {
527 lang_nc.cmd =
nullptr;
534 StrgenWarning(
"{}: command '{}' exists in template file but not in language file", name, templ_nc.cmd->cmd);
541 for (
size_t i = 0; i < templ.consuming_commands.max_size(); i++) {
542 if (TranslateCmdForCompare(templ.consuming_commands[i]) != lang.consuming_commands[i]) {
543 StrgenWarning(
"{}: Param idx #{} '{}' doesn't match with template command '{}'", name, i,
544 lang.consuming_commands[i] ==
nullptr ?
"<empty>" : TranslateCmdForCompare(lang.consuming_commands[i])->cmd,
545 templ.consuming_commands[i] == nullptr ?
"<empty>" : templ.consuming_commands[i]->cmd);
553void StringReader::HandleString(
char *str)
556 if (str[1] ==
'#' && str[2] !=
'#') this->
HandlePragma(str + 2, _strgen.
lang);
561 if (*str ==
';' || *str ==
' ' || *str ==
'\0')
return;
563 char *s = strchr(str,
':');
565 StrgenError(
"Line has no ':' delimiter");
572 for (t = s; t > str && (t[-1] ==
' ' || t[-1] ==
'\t'); t--) {}
578 for (tmp = s; *tmp !=
'\0';) {
579 size_t len = Utf8Validate(tmp);
580 if (len == 0) StrgenFatal(
"Invalid UTF-8 sequence in '{}'", s);
586 (c >= 0xE000 && c <= 0xF8FF) ||
587 (c >= 0xFFF0 && c <= 0xFFFF)) {
588 StrgenFatal(
"Unwanted UTF-8 character U+{:04X} in sequence '{}'",
static_cast<uint32_t
>(c), s);
596 char *casep = strchr(str,
'.');
597 if (casep !=
nullptr) *casep++ =
'\0';
603 if (casep !=
nullptr) {
604 StrgenError(
"Cases in the base translation are not supported.");
608 if (ent !=
nullptr) {
609 StrgenError(
"String name '{}' is used multiple times", str);
613 if (this->
data.
strings[this->data.next_string_id] !=
nullptr) {
614 StrgenError(
"String ID 0x{:X} for '{}' already in use by '{}'", this->
data.
next_string_id, str, this->data.strings[this->data.next_string_id]->name);
621 if (ent ==
nullptr) {
622 StrgenWarning(
"String name '{}' does not exist in master file", str);
626 if (!ent->
translated.empty() && casep ==
nullptr) {
627 StrgenError(
"String name '{}' is used multiple times", str);
632 if (!CheckCommandsMatch(s, ent->
english.c_str(), str))
return;
634 if (casep !=
nullptr) {
648 if (!memcmp(str,
"plural ", 7)) {
651 StrgenFatal(
"Invalid pluralform {}", lang.
plural_form);
654 StrgenFatal(
"unknown pragma '{}'", str);
658static void StripTrailingWhitespace(std::string &str)
660 str.erase(str.find_last_not_of(
"\r\n ") + 1);
665 _strgen.warnings = _strgen.errors = 0;
678 std::optional<std::string> line = this->
ReadLine();
679 if (!line.has_value())
return;
681 StripTrailingWhitespace(line.value());
682 this->HandleString(line.value().data());
687 StrgenError(
"Too many strings, maximum allowed is {}", this->
data.
max_strings);
699 if (data.
strings[i] !=
nullptr) {
708static size_t TranslateArgumentIdx(
size_t argidx,
size_t offset)
710 if (argidx >= _cur_pcs.consuming_commands.max_size()) {
711 StrgenFatal(
"invalid argidx {}", argidx);
713 const CmdStruct *cs = _cur_pcs.consuming_commands[argidx];
714 if (cs !=
nullptr && cs->consumes <= offset) {
715 StrgenFatal(
"invalid argidx offset {}:{}", argidx, offset);
718 if (_cur_pcs.consuming_commands[argidx] ==
nullptr) {
719 StrgenFatal(
"no command for this argidx {}", argidx);
723 for (
size_t i = 0; i < argidx; i++) {
724 cs = _cur_pcs.consuming_commands[i];
726 sum += (cs !=
nullptr) ? cs->consumes : 1;
734 builder.
PutUtf8(SCC_ARG_INDEX);
735 builder.
PutUint8(
static_cast<uint8_t
>(TranslateArgumentIdx(_cur_argidx)));
738static std::string PutCommandString(
const char *str)
744 while (*str !=
'\0') {
751 auto cs = ParseCommandString(&str);
753 if (cmd ==
nullptr)
break;
755 if (cs.casei.has_value()) {
761 if (cmd->consumes > 0) {
763 if (cs.argno.has_value() && *cs.argno != _cur_argidx) {
764 _cur_argidx = *cs.argno;
765 PutArgidxCommand(builder);
769 cmd = _cur_pcs.consuming_commands[_cur_argidx++];
770 if (cmd ==
nullptr) {
771 StrgenFatal(
"{}: No argument exists at position {}", _cur_ident, _cur_argidx - 1);
775 cmd->proc(builder, cs.param.c_str(), cmd->value);
788 if (length >= 0x4000) {
789 StrgenFatal(
"string too long");
792 if (length >= 0xC0) {
793 buffer[offs++] =
static_cast<char>(
static_cast<uint8_t
>((length >> 8) | 0xC0));
795 buffer[offs++] =
static_cast<char>(
static_cast<uint8_t
>(length & 0xFF));
796 this->
Write(buffer, offs);
805 std::vector<size_t> in_use;
806 for (
size_t tab = 0; tab < data.
tabs; tab++) {
810 _strgen.
lang.
offsets[tab] = TO_LE16(
static_cast<uint16_t
>(n));
812 for (
size_t j = 0; j != in_use[tab]; j++) {
825 for (
size_t tab = 0; tab < data.
tabs; tab++) {
826 for (
size_t j = 0; j != in_use[tab]; j++) {
837 _cur_ident = ls->
name.c_str();
842 if (_strgen.show_warnings) {
843 StrgenWarning(
"'{}' is untranslated", ls->
name);
845 if (_strgen.annotate_todos) {
846 builder.
Put(
"<TODO> ");
851 _cur_pcs = ExtractCommandString(ls->
english.c_str(),
false);
861 builder.
PutUtf8(SCC_SWITCH_CASE);
866 auto case_str = PutCommandString(c.string.c_str());
868 builder.
PutUint16LE(
static_cast<uint16_t
>(case_str.size()));
869 builder.
Put(case_str);
874 if (!cmdp.empty()) def_str = PutCommandString(cmdp.c_str());
876 builder.
PutUint16LE(
static_cast<uint16_t
>(def_str.size()));
878 builder.
Put(def_str);
881 this->
Write(output.data(), output.size());
debug_inline constexpr bool HasBit(const T x, const uint8_t y)
Checks if a bit in a value is set.
debug_inline 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 bool Test(Tvalue_type value) const
Test if the value-th bit is set.
void PutUtf8(char32_t c)
Append UTF.8 char.
void PutUint16LE(uint16_t value)
Append binary uint16 using little endian.
void Put(std::string_view str)
Append string.
void PutChar(char c)
Append 8-bit char.
void PutUint8(uint8_t value)
Append binary uint8.
Compose data into a growing std::string.
static const uint8_t MAX_NUM_GENDERS
Maximum number of supported genders.
static const uint8_t MAX_NUM_CASES
Maximum number of supported cases.
constexpr bool IsInsideBS(const T x, const size_t base, const size_t size)
Checks if a value is between a window started at some base point.
void MemSetT(T *ptr, uint8_t value, size_t num=1)
Type-safe version of memset().
#define lengthof(array)
Return the length of an fixed size array.
Structures related to strgen.
static bool _translated
Whether the current language is not the master language.
static uint32_t VersionHashStr(uint32_t hash, std::string_view s)
Create a compound hash.
static const PluralForm _plural_forms[]
All plural forms used.
@ Gender
These commands support genders.
@ Case
These commands support cases.
@ DontCount
These commands aren't counted for comparison.
void strecpy(std::span< char > dst, std::string_view src)
Copies characters from one buffer to another.
size_t Utf8Decode(char32_t *c, const char *s)
Decode and consume the next UTF-8 encoded character.
static const uint TAB_SIZE
Number of strings per StringTab.
Container for the different cases of a string.
Case(uint8_t caseidx, const std::string &string)
Create a new case.
Information about a single string.
size_t line
Line of string in source-file.
std::string english
English text.
std::vector< Case > translated_cases
Cases of the translation.
std::string translated
Translated text.
void FreeTranslation()
Free all data related to the translation.
std::string name
Name of the string.
LangString(const std::string &name, const std::string &english, size_t index, size_t line)
Create a new string.
virtual void WriteHeader(const LanguagePackHeader *header)=0
Write the header metadata.
virtual void WriteLength(size_t length)
Write the length as a simple gamma.
virtual void Write(const char *buffer, size_t length)=0
Write a number of bytes.
virtual void WriteLang(const StringData &data)
Actually write the language.
Global state shared between strgen.cpp, game_text.cpp and strgen_base.cpp.
std::string file
The filename of the input, so we can refer to it in errors/warnings.
bool translation
Is the current file actually a translation or not.
LanguagePackHeader lang
Header information about a language.
size_t cur_line
The current line we're parsing in the input file.
Information about the currently known strings.
size_t tabs
The number of 'tabs' of strings.
std::unordered_map< std::string, std::shared_ptr< LangString > > name_to_string
Lookup table for the strings.
void Add(std::shared_ptr< LangString > ls)
Add a newly created LangString.
size_t max_strings
The maximum number of strings.
size_t next_string_id
The next string ID to allocate.
void FreeTranslation()
Free all data related to the translation.
StringData(size_t tabs)
Create a new string data container.
LangString * Find(const std::string &s)
Find a LangString based on the string name.
std::vector< std::shared_ptr< LangString > > strings
List of all known strings.
uint32_t Version() const
Make a hash of the file to get a unique "version number".
size_t CountInUse(size_t tab) const
Count the number of tab elements that are in use.
const std::string file
The file we are reading.
StringReader(StringData &data, const std::string &file, bool master, bool translation)
Prepare reading.
StringData & data
The data to fill during reading.
virtual void HandlePragma(char *str, LanguagePackHeader &lang)
Handle the pragma of the file.
virtual void ParseFile()
Start parsing the file.
bool translation
Are we reading a translation, implies !master. However, the base translation will have this false.
virtual std::optional< std::string > ReadLine()=0
Read a single line from the source of strings.
bool master
Are we reading the master file?