VVVVVV/desktop_version/src/LocalizationStorage.cpp

1140 lines
32 KiB
C++

#define LOCALIZATIONSTORAGE_CPP
#include "Localization.h"
#include "LocalizationStorage.h"
#include "Alloc.h"
#include "Constants.h"
#include "CustomLevels.h"
#include "FileSystemUtils.h"
#include "Font.h"
#include "Graphics.h"
#include "Unused.h"
#include "UtilityClass.h"
#include "VFormat.h"
#include "Vlogging.h"
namespace loc
{
bool inited = false;
bool inited_custom = false;
char* custom_level_path = NULL;
std::vector<TextOverflow> text_overflows;
bool load_lang_doc(
const std::string& cat,
tinyxml2::XMLDocument& doc,
const std::string& langcode /*= lang*/,
const std::string& asset_cat /*= ""*/
)
{
/* Load a language-related XML file.
* cat is the "category", so "strings", "numbers", etc.
*
* asset_cat is only used when loading
* from custom level assets is possible. */
bool asset_loaded = false;
if (!asset_cat.empty())
{
asset_loaded = FILESYSTEM_loadAssetTiXml2Document(("lang/" + langcode + "/" + asset_cat + ".xml").c_str(), doc);
}
if (!asset_loaded && !FILESYSTEM_loadTiXml2Document(("lang/" + langcode + "/" + cat + ".xml").c_str(), doc))
{
vlog_debug("Could not load language file %s/%s.", langcode.c_str(), cat.c_str());
return false;
}
if (doc.Error())
{
vlog_error("Error parsing language file %s/%s: %s", langcode.c_str(), cat.c_str(), doc.ErrorStr());
return false;
}
return true;
}
static void loadmeta(LangMeta& meta, const std::string& langcode = lang)
{
meta.active = true;
meta.code = langcode;
meta.nativename = langcode;
meta.credit = "";
meta.action_hint = "Press Space, Z, or V to select";
meta.gamepad_hint = "Press {button} to select";
meta.autowordwrap = true;
meta.toupper = true;
meta.toupper_i_dot = false;
meta.toupper_lower_escape_char = false;
meta.rtl = false;
meta.menu_select = "[ {label} ]";
meta.menu_select_tight = "[{label}]";
meta.font_idx = font::get_font_idx_8x8();
tinyxml2::XMLDocument doc;
tinyxml2::XMLHandle hDoc(&doc);
tinyxml2::XMLElement* pElem;
if (!load_lang_doc("meta", doc, langcode))
{
return;
}
FOR_EACH_XML_ELEMENT(hDoc, pElem)
{
const char* pKey = pElem->Value();
const char* pText = pElem->GetText();
if (pText == NULL)
{
pText = "";
}
if (SDL_strcmp(pKey, "active") == 0)
meta.active = help.Int(pText);
else if (SDL_strcmp(pKey, "nativename") == 0)
meta.nativename = std::string(pText);
else if (SDL_strcmp(pKey, "credit") == 0)
meta.credit = std::string(pText);
else if (SDL_strcmp(pKey, "action_hint") == 0)
meta.action_hint = std::string(pText);
else if (SDL_strcmp(pKey, "gamepad_hint") == 0)
meta.gamepad_hint = std::string(pText);
else if (SDL_strcmp(pKey, "autowordwrap") == 0)
meta.autowordwrap = help.Int(pText);
else if (SDL_strcmp(pKey, "toupper") == 0)
meta.toupper = help.Int(pText);
else if (SDL_strcmp(pKey, "toupper_i_dot") == 0)
meta.toupper_i_dot = help.Int(pText);
else if (SDL_strcmp(pKey, "toupper_lower_escape_char") == 0)
meta.toupper_lower_escape_char = help.Int(pText);
else if (SDL_strcmp(pKey, "rtl") == 0)
meta.rtl = help.Int(pText);
else if (SDL_strcmp(pKey, "menu_select") == 0)
meta.menu_select = std::string(pText);
else if (SDL_strcmp(pKey, "menu_select_tight") == 0)
meta.menu_select_tight = std::string(pText);
else if (SDL_strcmp(pKey, "font") == 0)
font::find_main_font_by_name(pText, &meta.font_idx);
}
}
static void map_store_translation(Textbook* textbook, hashmap* map, const char* eng, const char* tra)
{
/* Add the texts to the given textbook and set the translation in the given hashmap. */
if (eng == NULL)
{
return;
}
if (tra == NULL)
{
tra = "";
}
const char* tb_eng = textbook_store(textbook, eng);
const char* tb_tra = textbook_store(textbook, tra);
if (tb_eng == NULL || tb_tra == NULL)
{
return;
}
hashmap_set(map, tb_eng, SDL_strlen(tb_eng), (uintptr_t) tb_tra);
}
unsigned char form_for_count(int n)
{
int n_ix;
if (n > -100 && n < 100)
{
/* Plural forms for negative numbers are debatable in any language I'd imagine...
* But they shouldn't appear anyway unless there's a bug or you're asking for it.
* Or do YOU ever get -10 deaths while collecting -1 trinket? */
n_ix = SDL_abs(n);
}
else
{
/* Plural forms for 100 and above always just keep repeating. Thank goodness. */
n_ix = SDL_abs(n % 100) + 100;
}
return number_plural_form[n_ix];
}
static void callback_free_map_value(void* key, size_t ksize, uintptr_t value, void* usr)
{
UNUSED(key);
UNUSED(ksize);
UNUSED(usr);
hashmap_free((hashmap*) value);
}
static void resettext_custom(bool final_shutdown)
{
/* Reset/initialize custom level strings only.
* If final_shutdown, this just does a last cleanup of any allocations,
* otherwise it makes storage ready for first use (or reuse by a new language). */
if (inited_custom)
{
hashmap_iterate(map_translation_cutscene_custom, callback_free_map_value, NULL);
hashmap_free(map_translation_cutscene_custom);
textbook_clear(&textbook_custom);
}
else if (!final_shutdown)
{
inited_custom = true;
textbook_init(&textbook_custom);
}
if (!final_shutdown)
{
map_translation_cutscene_custom = hashmap_create();
SDL_zeroa(translation_roomnames_custom);
SDL_zeroa(explanation_roomnames_custom);
n_untranslated_roomnames_custom = 0;
n_unexplained_roomnames_custom = 0;
}
}
void unloadtext_custom(void)
{
resettext_custom(false);
loc::lang_custom = "";
VVV_free(custom_level_path);
custom_level_path = NULL;
}
void resettext(bool final_shutdown)
{
/* Reset/initialize strings.
* If final_shutdown, this just does a last cleanup of any allocations,
* otherwise it makes storage ready for first use (or reuse by a new language). */
if (inited)
{
hashmap_free(map_translation);
hashmap_iterate(map_translation_cutscene, callback_free_map_value, NULL);
hashmap_free(map_translation_cutscene);
hashmap_free(map_translation_plural);
hashmap_free(map_translation_roomnames_special);
textbook_clear(&textbook_main);
}
else if (!final_shutdown)
{
inited = true;
textbook_init(&textbook_main);
}
if (!final_shutdown)
{
map_translation = hashmap_create();
map_translation_cutscene = hashmap_create();
map_translation_plural = hashmap_create();
for (size_t i = 0; i <= 100; i++)
{
number[i] = "";
number2[i] = "";
}
SDL_zeroa(number_plural_form);
number_plural_form[1] = 1;
SDL_zeroa(translation_roomnames);
SDL_zeroa(explanation_roomnames);
n_untranslated_roomnames = 0;
n_unexplained_roomnames = 0;
SDL_zeroa(n_untranslated_roomnames_area);
SDL_zeroa(n_untranslated);
map_translation_roomnames_special = hashmap_create();
}
resettext_custom(final_shutdown);
}
bool parse_max(const char* max, unsigned short* max_w, unsigned short* max_h)
{
/* Parse a max string, like "33" or "33*3", into two shorts.
* Returns true if successful and max_w/max_h have gotten valid values, false otherwise. */
if (max == NULL)
{
return false;
}
char* max_mut = SDL_strdup(max);
if (max_mut == NULL)
{
return false;
}
char* asterisk = SDL_strchr(max_mut, '*');
if (asterisk != NULL)
{
asterisk[0] = '\0';
*max_h = (unsigned short) help.Int(&asterisk[1], 0);
}
else
{
*max_h = 1;
}
*max_w = (unsigned short) help.Int(max_mut, 0);
VVV_free(max_mut);
return *max_w != 0 && *max_h != 0;
}
static bool max_check_string(const char* str, const char* max)
{
/* Stores a detected overflow in the overflows vector, returns true if this happened */
unsigned short max_w, max_h;
if (str == NULL || !parse_max(max, &max_w, &max_h))
{
return false;
}
/* Special case that must ALWAYS be 2 lines even when the font is bigger */
if (SDL_strcmp(str, "You have rescued a crew member!") == 0 && max_h == 1)
{
max_h = 2;
}
uint8_t font_idx = get_langmeta()->font_idx;
uint32_t print_flags = PR_FONT_IDX(font_idx, get_langmeta()->rtl) | PR_CJK_LOW;
uint8_t font_w = 8;
uint8_t font_h = 8;
font::glyph_dimensions(print_flags, &font_w, &font_h);
unsigned short max_w_px = max_w * 8;
unsigned short max_h_px = max_h * 10;
bool does_overflow = false;
if (max_h == 1)
{
max_h_px = font_h;
does_overflow = font::len(print_flags, str) > (int) max_w_px;
}
else
{
short lines;
font::string_wordwrap(print_flags, str, max_w_px, &lines);
does_overflow = lines*SDL_max(10, font_h) > (short) max_h_px;
}
// Convert max_w and max_h from 8x8 into local
max_w = max_w_px / font_w;
max_h = max_h_px / SDL_max(10, font_h);
if (does_overflow)
{
TextOverflow overflow;
overflow.lang = lang;
overflow.text = textbook_store(&textbook_main, str);
overflow.max_w = max_w;
overflow.max_h = max_h;
overflow.max_w_px = max_w_px;
overflow.max_h_px = max_h_px;
overflow.multiline = max_h > 1;
overflow.flags = print_flags;
text_overflows.push_back(overflow);
vlog_warn("\"%s\" DOESN'T FIT into %s which is %dx%d or %dx%dpx",
str, max, max_w, max_h, max_w_px, max_h_px
);
}
else
{
vlog_debug("\"%s\" fits into %s which is %dx%d or %dx%dpx",
str, max, max_w, max_h, max_w_px, max_h_px
);
}
return does_overflow;
}
static void max_check_string_plural(unsigned char form, const char* str, const char* max, const char* var, unsigned int expect)
{
if (str == NULL || var == NULL)
{
return;
}
/* Create an args index from just the name of the variable.
* Also get rid of all other placeholders.*/
char args_index[60];
vformat_buf(args_index, sizeof(args_index), "{var}:int, _:int", "var:str", var);
char buf[20*SCREEN_WIDTH_CHARS + 1];
if (expect > 100)
{
/* Treat `expect` as a single example, it's the number of digits that's most important */
if (form_for_count(expect) == form)
{
vformat_buf(buf, sizeof(buf), str, args_index, expect, 0);
max_check_string(buf, max);
}
}
else
{
/* Test all numbers from 0 to `expect`, since if we have wordy numbers, they have differing lengths */
for (unsigned int test = 0; test <= expect; test++)
{
if (form_for_count(test) == form)
{
vformat_buf(buf, sizeof(buf), str, args_index, test, 0);
if (max_check_string(buf, max))
{
/* One is enough */
break;
}
}
}
}
}
static void tally_untranslated(const char* tra, int* counter)
{
/* Count this translation in the untranslated count if it's untranslated. */
if (!show_translator_menu)
{
return;
}
if (tra == NULL || tra[0] == '\0')
{
(*counter)++;
}
}
static void loadtext_strings(bool check_max)
{
tinyxml2::XMLDocument doc;
tinyxml2::XMLHandle hDoc(&doc);
tinyxml2::XMLElement* pElem;
if (!load_lang_doc("strings", doc))
{
return;
}
FOR_EACH_XML_ELEMENT(hDoc, pElem)
{
EXPECT_ELEM(pElem, "string");
const char* eng = pElem->Attribute("english");
const char* tra = pElem->Attribute("translation");
char textcase = pElem->UnsignedAttribute("case", 0);
if (textcase == 0)
{
map_store_translation(
&textbook_main,
map_translation,
eng,
tra
);
}
else
{
/* Only prefix with a disambiguator if a specific case number is set */
char* eng_prefixed = add_disambiguator(textcase, eng, NULL);
if (eng_prefixed == NULL)
{
continue;
}
map_store_translation(
&textbook_main,
map_translation,
eng_prefixed,
tra
);
VVV_free(eng_prefixed);
}
/* Only tally an untranslated string if English isn't blank */
if (eng != NULL && eng[0] != '\0')
{
tally_untranslated(tra, &n_untranslated[UNTRANSLATED_STRINGS]);
}
if (check_max)
{
/* VFormat placeholders distort the limits check.
* (max_check_string ignores NULL strings.) */
char* filled = vformat_alloc(tra, "_:int", 0);
max_check_string(filled, pElem->Attribute("max"));
VVV_free(filled);
}
}
}
static void loadtext_strings_plural(bool check_max)
{
tinyxml2::XMLDocument doc;
tinyxml2::XMLHandle hDoc(&doc);
tinyxml2::XMLElement* pElem;
if (!load_lang_doc("strings_plural", doc))
{
return;
}
FOR_EACH_XML_ELEMENT(hDoc, pElem)
{
EXPECT_ELEM(pElem, "string");
const char* eng_plural = pElem->Attribute("english_plural");
if (eng_plural == NULL)
{
continue;
}
tinyxml2::XMLElement* subElem;
FOR_EACH_XML_SUB_ELEMENT(pElem, subElem)
{
EXPECT_ELEM(subElem, "translation");
unsigned char form = subElem->IntAttribute("form", 0);
char* key = add_disambiguator(form+1, eng_plural, NULL);
if (key == NULL)
{
continue;
}
map_store_translation(
&textbook_main,
map_translation_plural,
key,
subElem->Attribute("translation")
);
VVV_free(key);
tally_untranslated(subElem->Attribute("translation"), &n_untranslated[UNTRANSLATED_STRINGS_PLURAL]);
if (check_max)
{
max_check_string_plural(
form, subElem->Attribute("translation"),
pElem->Attribute("max"),
pElem->Attribute("var"), pElem->UnsignedAttribute("expect", 101)
);
}
}
}
}
static bool get_level_lang_path(bool custom_level, const char* cat, std::string& doc_path, std::string& doc_path_asset)
{
/* Calculate the path to a translation file for either the MAIN GAME or
* a CUSTOM LEVEL. cat can be "roomnames", "cutscenes", etc.
*
* doc_path and doc_path_asset are "out" parameters, and will be set to
* the appropriate filenames to use for language files outside of or
* inside level assets respectively (translations for custom levels can
* live in the main language folders too)
*
* Returns whether this is a (valid) custom level path. */
if (custom_level
&& custom_level_path != NULL
&& SDL_strncmp(custom_level_path, "levels/", 7) == 0
&& SDL_strlen(custom_level_path) > (sizeof(".vvvvvv")-1)
)
{
/* Get rid of .vvvvvv */
size_t len = SDL_strlen(custom_level_path)-7;
doc_path = std::string(custom_level_path, len);
doc_path.append("/custom_");
doc_path.append(cat);
/* For the asset path, also get rid of the levels/LEVELNAME/ */
doc_path_asset = "custom_";
doc_path_asset.append(cat);
return true;
}
else
{
doc_path = cat;
doc_path_asset = "";
return false;
}
}
const char* get_level_original_lang(tinyxml2::XMLHandle& hDoc)
{
/* cutscenes and roomnames files can specify the original language as
* an attribute of the root tag to change the attribute names of the
* original text (normally "english"). This makes level translations
* less confusing if the original language isn't English. */
const char* original = NULL;
tinyxml2::XMLElement* pRoot = hDoc.FirstChildElement().ToElement();
if (pRoot != NULL)
{
original = pRoot->Attribute("original");
}
if (original == NULL)
{
original = "english";
}
return original;
}
static std::string& get_level_lang_code(bool custom_level)
{
if (!custom_level || lang_custom == "")
{
return lang;
}
return lang_custom;
}
static void loadtext_cutscenes(bool custom_level)
{
tinyxml2::XMLDocument doc;
tinyxml2::XMLHandle hDoc(&doc);
tinyxml2::XMLElement* pElem;
std::string doc_path;
std::string doc_path_asset;
bool valid_custom_level = get_level_lang_path(custom_level, "cutscenes", doc_path, doc_path_asset);
if (custom_level && !valid_custom_level)
{
return;
}
if (!load_lang_doc(doc_path, doc, get_level_lang_code(custom_level), doc_path_asset))
{
return;
}
Textbook* textbook;
hashmap* map;
if (custom_level)
{
textbook = &textbook_custom;
map = map_translation_cutscene_custom;
}
else
{
textbook = &textbook_main;
map = map_translation_cutscene;
}
const char* original = get_level_original_lang(hDoc);
FOR_EACH_XML_ELEMENT(hDoc, pElem)
{
EXPECT_ELEM(pElem, "cutscene");
const char* script_id = textbook_store(textbook, pElem->Attribute("id"));
if (script_id == NULL)
{
continue;
}
hashmap* cutscene_map = hashmap_create();
hashmap_set_free(
map,
script_id,
SDL_strlen(script_id),
(uintptr_t) cutscene_map,
callback_free_map_value,
NULL
);
tinyxml2::XMLElement* subElem;
FOR_EACH_XML_SUB_ELEMENT(pElem, subElem)
{
EXPECT_ELEM(subElem, "dialogue");
const char* eng = subElem->Attribute(original);
const char* tra = subElem->Attribute("translation");
if (!custom_level)
{
tally_untranslated(tra, &n_untranslated[UNTRANSLATED_CUTSCENES]);
}
if (eng == NULL || tra == NULL)
{
continue;
}
const std::string eng_unwrapped = font::string_unwordwrap(eng);
char* eng_prefixed = add_disambiguator(subElem->UnsignedAttribute("case", 1), eng_unwrapped.c_str(), NULL);
if (eng_prefixed == NULL)
{
continue;
}
const char* tb_eng = textbook_store(textbook, eng_prefixed);
const char* tb_tra = textbook_store(textbook, tra);
VVV_free(eng_prefixed);
if (tb_eng == NULL || tb_tra == NULL)
{
continue;
}
TextboxFormat format;
format.text = tb_tra;
format.tt = subElem->BoolAttribute("tt", false);
format.centertext = subElem->BoolAttribute("centertext", false);
format.pad_left = subElem->UnsignedAttribute("pad_left", 0);
format.pad_right = subElem->UnsignedAttribute("pad_right", 0);
unsigned short pad = subElem->UnsignedAttribute("pad", 0);
format.pad_left += pad;
format.pad_right += pad;
format.wraplimit_raw = subElem->UnsignedAttribute("wraplimit", 0);
format.wraplimit = format.wraplimit_raw;
if (format.wraplimit == 0)
{
format.wraplimit = 36*8 - (format.pad_left+format.pad_right)*8;
}
format.padtowidth = subElem->UnsignedAttribute("padtowidth", 0);
const TextboxFormat* tb_format = (TextboxFormat*) textbook_store_raw(
textbook,
&format,
sizeof(TextboxFormat)
);
if (tb_format == NULL)
{
continue;
}
hashmap_set(cutscene_map, tb_eng, SDL_strlen(tb_eng), (uintptr_t) tb_format);
}
}
}
static void loadtext_numbers(void)
{
tinyxml2::XMLDocument doc;
tinyxml2::XMLHandle hDoc(&doc);
tinyxml2::XMLElement* pElem;
if (!load_lang_doc("numbers", doc))
{
return;
}
FOR_EACH_XML_ELEMENT(hDoc, pElem)
{
EXPECT_ELEM(pElem, "number");
const char* value_str = pElem->Attribute("value");
int value = help.Int(value_str);
if (value >= 0 && value <= 100)
{
const char* tra = pElem->Attribute("translation");
if (tra == NULL)
{
tra = "";
}
number[value] = std::string(tra);
tally_untranslated(tra, &n_untranslated[UNTRANSLATED_NUMBERS]);
// FIXME: implement a more flexible system later, where translators define the classes
tra = pElem->Attribute("translation2");
if (tra == NULL)
{
tra = "";
}
number2[value] = std::string(tra);
}
if (value >= 0 && value <= 199)
{
int form = pElem->IntAttribute("form", 0);
number_plural_form[value] = form;
if (value < 100)
{
number_plural_form[value+100] = form;
}
}
}
}
bool fix_room_coords(bool custom_level, int* roomx, int* roomy)
{
*roomx %= 100;
*roomy %= 100;
if (!custom_level && *roomx == 9 && *roomy == 4)
{
// The Tower has two rooms, unify them
*roomy = 9;
}
int max_x = MAP_MAX_X;
int max_y = MAP_MAX_Y;
if (custom_level)
{
max_x = CUSTOM_MAP_MAX_X;
max_y = CUSTOM_MAP_MAX_Y;
}
return !(*roomx < 0 || *roomy < 0 || *roomx > max_x || *roomy > max_y);
}
static unsigned coords_to_area(int roomx, int roomy)
{
if (!fix_room_coords(false, &roomx, &roomy))
{
return false;
}
/* We want to know per-area how many room names are untranslated... */
enum area_letter {
_, /* None */
S, /* SS1 */
L, /* Lab */
T, /* Tower */
Y, /* SS2 */
W, /* Warp */
I, /* Intermission */
G, /* Gravitron */
F /* Final */
};
static enum area_letter area_map[MAP_MAX_Y+1][MAP_MAX_Y+1] = {
{_,L,L,L,L,L,L,L,_,T,_,_,_,W,W,W,W,W,W,W},
{_,L,L,L,L,L,L,_,_,T,_,_,_,_,W,W,W,W,W,W},
{_,_,_,_,L,_,_,_,_,T,_,_,_,_,W,W,W,W,W,W},
{_,_,_,_,L,_,_,_,_,T,_,_,S,S,S,S,W,W,W,W},
{_,_,L,L,L,_,_,_,_,T,T,T,S,S,S,S,_,_,_,_},
{_,_,_,_,_,_,_,_,_,T,Y,Y,S,S,S,S,_,_,_,_},
{_,_,_,_,_,_,_,_,_,T,Y,Y,S,S,S,S,S,_,_,_},
{_,_,_,_,_,_,_,_,_,T,Y,Y,S,S,S,S,S,S,S,_},
{_,_,_,_,_,_,_,_,_,T,_,_,_,Y,Y,S,Y,Y,Y,_},
{_,_,_,_,_,_,_,_,T,T,_,_,_,Y,Y,Y,Y,Y,Y,_},
{_,_,_,_,_,_,_,_,_,T,_,_,_,Y,Y,Y,Y,Y,Y,_},
{_,_,_,_,_,_,_,_,_,T,_,Y,Y,Y,Y,Y,Y,Y,Y,_},
{_,_,_,_,_,_,_,_,_,T,_,Y,Y,Y,Y,Y,Y,_,Y,_},
{_,_,_,_,_,_,_,_,_,T,_,Y,Y,Y,Y,Y,Y,_,Y,_},
{_,_,_,_,_,_,_,_,_,T,_,Y,Y,_,_,_,_,_,Y,_},
{_,_,_,_,_,_,_,L,_,T,_,_,_,_,_,_,_,_,_,_},
{_,_,L,L,L,L,L,L,_,T,_,_,_,_,_,_,_,_,_,_},
{_,L,L,L,L,L,L,L,_,T,_,_,_,_,_,_,_,_,_,_},
{L,L,L,L,L,_,_,L,_,T,_,_,_,_,_,_,_,_,_,_},
{L,L,L,L,L,_,_,L,_,T,_,_,_,_,_,_,_,_,_,_}
};
static bool area_map_has_final = false;
if (!area_map_has_final)
{
static const enum area_letter final_map[9][14] = {
{_,_,_,_,_,_,_,_,_,_,_,_,G,F},
{_,_,_,_,_,_,_,_,_,_,_,_,G,F},
{_,_,_,_,_,_,_,_,_,_,_,_,G,F},
{F,F,F,F,F,F,F,F,F,F,_,_,G,F},
{F,F,F,F,F,_,F,F,F,F,_,_,G,F},
{_,_,_,_,_,_,_,_,F,F,F,F,F,F},
{_,_,_,_,_,F,F,F,F,F,F,_,_,_},
{_,_,_,_,_,_,_,_,_,_,_,_,_,_},
{I,I,I,I,I,I,I,I,I,I,I,I,I,I}
};
for (int y = 0; y < 9; y++)
{
for (int x = 0; x < 14; x++)
{
area_map[MAP_MAX_Y+1 - 9 + y][MAP_MAX_X+1 - 14 + x] = final_map[y][x];
}
}
area_map_has_final = true;
}
if (area_map[roomy][roomx] == 0)
{
vlog_error("LocalizationStorage: Room %d,%d has no area associated with it", roomx, roomy);
}
return area_map[roomy][roomx];
}
static void update_left_counter(const char* old_text, const char* new_text, int* counter, int* counter_area)
{
bool now_filled = new_text[0] != '\0';
if ((old_text == NULL || old_text[0] == '\0') && now_filled)
{
(*counter)--;
if (counter_area != NULL)
{
(*counter_area)--;
}
}
else if (old_text != NULL && old_text[0] != '\0' && !now_filled)
{
(*counter)++;
if (counter_area != NULL)
{
(*counter_area)++;
}
}
}
bool store_roomname_translation(bool custom_level, int roomx, int roomy, const char* tra, const char* explanation)
{
if (!fix_room_coords(custom_level, &roomx, &roomy))
{
return false;
}
/* We have some arrays filled with pointers, and we need to change those pointers */
const char** ptr_translation;
const char** ptr_explanation;
int* ptr_n_untranslated;
int* ptr_n_untranslated_area = NULL;
int* ptr_n_unexplained;
if (custom_level)
{
ptr_translation = &translation_roomnames_custom[roomy][roomx];
ptr_explanation = &explanation_roomnames_custom[roomy][roomx];
ptr_n_untranslated = &n_untranslated_roomnames_custom;
ptr_n_unexplained = &n_unexplained_roomnames_custom;
}
else
{
ptr_translation = &translation_roomnames[roomy][roomx];
ptr_explanation = &explanation_roomnames[roomy][roomx];
ptr_n_untranslated = &n_untranslated_roomnames;
ptr_n_untranslated_area = &n_untranslated_roomnames_area[coords_to_area(roomx, roomy)];
ptr_n_unexplained = &n_unexplained_roomnames;
}
if (tra != NULL)
{
update_left_counter(*ptr_translation, tra, ptr_n_untranslated, ptr_n_untranslated_area);
*ptr_translation = textbook_store(&textbook_main, tra);
}
if (explanation != NULL)
{
update_left_counter(*ptr_explanation, explanation, ptr_n_unexplained, NULL);
*ptr_explanation = textbook_store(&textbook_main, explanation);
}
return true;
}
static void loadtext_roomnames(bool custom_level, bool check_max)
{
tinyxml2::XMLDocument doc;
tinyxml2::XMLHandle hDoc(&doc);
tinyxml2::XMLElement* pElem;
std::string doc_path;
std::string doc_path_asset;
bool valid_custom_level = get_level_lang_path(custom_level, "roomnames", doc_path, doc_path_asset);
if (custom_level && !valid_custom_level)
{
return;
}
if (!load_lang_doc(doc_path, doc, get_level_lang_code(custom_level), doc_path_asset))
{
return;
}
const char* original = get_level_original_lang(hDoc);
FOR_EACH_XML_ELEMENT(hDoc, pElem)
{
EXPECT_ELEM(pElem, "roomname");
int x = pElem->IntAttribute("x", -1);
int y = pElem->IntAttribute("y", -1);
if (custom_level)
{
/* Extra safeguard: make sure the original room name matches! */
const char* original_roomname = pElem->Attribute(original);
if (original_roomname == NULL)
{
continue;
}
const RoomProperty* const room = cl.getroomprop(x, y);
if (SDL_strcmp(original_roomname, room->roomname.c_str()) != 0)
{
continue;
}
n_untranslated_roomnames_custom++;
n_unexplained_roomnames_custom++;
}
else
{
n_untranslated_roomnames++;
n_unexplained_roomnames++;
n_untranslated_roomnames_area[coords_to_area(x, y)]++;
if (check_max)
{
max_check_string(pElem->Attribute("translation"), "40");
}
}
store_roomname_translation(
custom_level,
x,
y,
pElem->Attribute("translation"),
show_translator_menu ? pElem->Attribute("explanation") : NULL
);
}
}
static void loadtext_roomnames_special(bool check_max)
{
tinyxml2::XMLDocument doc;
tinyxml2::XMLHandle hDoc(&doc);
tinyxml2::XMLElement* pElem;
if (!load_lang_doc("roomnames_special", doc))
{
return;
}
FOR_EACH_XML_ELEMENT(hDoc, pElem)
{
EXPECT_ELEM(pElem, "roomname");
map_store_translation(
&textbook_main,
map_translation_roomnames_special,
pElem->Attribute("english"),
pElem->Attribute("translation")
);
if (check_max)
{
max_check_string(pElem->Attribute("translation"), "40");
}
tally_untranslated(pElem->Attribute("translation"), &n_untranslated[UNTRANSLATED_ROOMNAMES_SPECIAL]);
}
}
void loadtext_custom(const char* custom_path)
{
resettext_custom(false);
if (custom_level_path == NULL && custom_path != NULL)
{
custom_level_path = SDL_strdup(custom_path);
}
loadtext_cutscenes(true);
loadtext_roomnames(true, false);
}
void loadtext(bool check_max)
{
resettext(false);
loadmeta(langmeta);
if (lang == "en")
{
if (show_translator_menu)
{
// We may still need the room name explanations
loadtext_roomnames(false, false);
n_untranslated_roomnames = 0;
SDL_zeroa(n_untranslated_roomnames_area);
}
}
else
{
loadtext_numbers();
loadtext_strings(check_max);
loadtext_strings_plural(check_max);
loadtext_cutscenes(false);
loadtext_roomnames(false, check_max);
loadtext_roomnames_special(check_max);
}
if (custom_level_path != NULL)
{
loadtext_custom(NULL);
}
}
void loadlanguagelist(void)
{
// Load the list of languages for the language screen
languagelist.clear();
size_t opt = 0;
languagelist_curlang = 0;
EnumHandle handle = {};
const char* code;
while ((code = FILESYSTEM_enumerateLanguageCodes(&handle)) != NULL)
{
LangMeta meta;
loadmeta(meta, code);
if (meta.active)
{
languagelist.push_back(meta);
if (SDL_strcmp(lang.c_str(), code) == 0)
{
languagelist_curlang = opt;
}
opt++;
}
}
FILESYSTEM_freeEnumerate(&handle);
}
const char* map_lookup_text(hashmap* map, const char* eng, const char* fallback)
{
uintptr_t ptr_tra;
bool found = hashmap_get(map, eng, SDL_strlen(eng), &ptr_tra);
const char* tra = (const char*) ptr_tra;
if (found && tra != NULL && tra[0] != '\0')
{
return tra;
}
return fallback;
}
char* add_disambiguator(char disambiguator, const char* original_string, size_t* ext_alloc_len)
{
/* Create a version of the string prefixed with the given byte.
* This byte is used when the English string is just not enough to identify the correct translation.
* It's needed to store plural forms, and when the same text appears multiple times in a cutscene.
* Caller must VVV_free. */
size_t alloc_len = 1+SDL_strlen(original_string)+1;
char* alloc = (char*) SDL_malloc(alloc_len);
if (alloc == NULL)
{
return NULL;
}
alloc[0] = disambiguator;
SDL_memcpy(&alloc[1], original_string, alloc_len-1);
if (ext_alloc_len != NULL)
{
*ext_alloc_len = alloc_len;
}
return alloc;
}
} /* namespace loc */