mirror of
https://github.com/TerryCavanagh/VVVVVV.git
synced 2024-12-22 17:49:43 +01:00
a2c3f47748
The language screen has a "Press Space, Z, or V to select" hint, which I forgot to update for supporting button glyphs in #943, so this commit does. <action_hint>Press Space, Z, or V to select</action_hint> <gamepad_hint>Press {button} to select</gamepad_hint>
568 lines
17 KiB
C++
568 lines
17 KiB
C++
#define LOCALIZATIONMAINT_CPP
|
|
#include "Localization.h"
|
|
#include "LocalizationStorage.h"
|
|
|
|
#include <tinyxml2.h>
|
|
|
|
#include "Alloc.h"
|
|
#include "FileSystemUtils.h"
|
|
#include "Font.h"
|
|
#include "Graphics.h"
|
|
#include "Script.h"
|
|
#include "Vlogging.h"
|
|
#include "XMLUtils.h"
|
|
|
|
|
|
namespace loc
|
|
{
|
|
|
|
static void write_max_local(tinyxml2::XMLElement* pElem, uint8_t glyph_w, uint8_t glyph_h)
|
|
{
|
|
const char* max;
|
|
if ((max = pElem->Attribute("max")) != NULL)
|
|
{
|
|
unsigned short max_w, max_h, max_local_w, max_local_h;
|
|
if (parse_max(max, &max_w, &max_h))
|
|
{
|
|
max_local_w = (max_w*8) / glyph_w;
|
|
max_local_h = (max_h*10) / SDL_max(10, glyph_h);
|
|
|
|
if (max_local_h == 0)
|
|
{
|
|
max_local_h = 1;
|
|
}
|
|
|
|
char buf[16];
|
|
if (max_h == 1)
|
|
{
|
|
SDL_snprintf(buf, sizeof(buf), "%d", max_local_w);
|
|
}
|
|
else
|
|
{
|
|
SDL_snprintf(buf, sizeof(buf), "%d*%d", max_local_w, max_local_h);
|
|
}
|
|
pElem->SetAttribute("max_local", buf);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void write_max_local_decl(tinyxml2::XMLDocument* doc, uint8_t glyph_w, uint8_t glyph_h)
|
|
{
|
|
char buf[16];
|
|
SDL_snprintf(buf, sizeof(buf), "%dx%d", glyph_w, glyph_h);
|
|
doc->FirstChildElement()->SetAttribute("max_local_for", buf);
|
|
}
|
|
|
|
static void sync_lang_file(const std::string& langcode)
|
|
{
|
|
/* Update translation files for the given language with new strings from templates.
|
|
* This basically takes the (English) templates, fills in existing translations, and saves.
|
|
* Any FILESYSTEM_saveTiXml2Document() writes to main lang dir */
|
|
vlog_info("Syncing %s with templates...", langcode.c_str());
|
|
|
|
lang = langcode;
|
|
loadtext(false);
|
|
|
|
uint8_t glyph_w = 8, glyph_h = 8;
|
|
font::glyph_dimensions(PR_FONT_IDX(langmeta.font_idx), &glyph_w, &glyph_h);
|
|
bool max_local_needed = glyph_w != 8 || glyph_h != 8;
|
|
|
|
tinyxml2::XMLDocument doc;
|
|
tinyxml2::XMLHandle hDoc(&doc);
|
|
tinyxml2::XMLElement* pElem;
|
|
tinyxml2::XMLElement* subElem;
|
|
|
|
if (load_lang_doc("meta", doc, "en"))
|
|
{
|
|
FOR_EACH_XML_ELEMENT(hDoc, pElem)
|
|
{
|
|
const char* pKey = pElem->Value();
|
|
|
|
if (SDL_strcmp(pKey, "active") == 0)
|
|
pElem->SetText((int) langmeta.active);
|
|
else if (SDL_strcmp(pKey, "nativename") == 0)
|
|
pElem->SetText(langmeta.nativename.c_str());
|
|
else if (SDL_strcmp(pKey, "credit") == 0)
|
|
pElem->SetText(langmeta.credit.c_str());
|
|
else if (SDL_strcmp(pKey, "action_hint") == 0)
|
|
pElem->SetText(langmeta.action_hint.c_str());
|
|
else if (SDL_strcmp(pKey, "gamepad_hint") == 0)
|
|
pElem->SetText(langmeta.gamepad_hint.c_str());
|
|
else if (SDL_strcmp(pKey, "autowordwrap") == 0)
|
|
pElem->SetText((int) langmeta.autowordwrap);
|
|
else if (SDL_strcmp(pKey, "toupper") == 0)
|
|
pElem->SetText((int) langmeta.toupper);
|
|
else if (SDL_strcmp(pKey, "toupper_i_dot") == 0)
|
|
pElem->SetText((int) langmeta.toupper_i_dot);
|
|
else if (SDL_strcmp(pKey, "toupper_lower_escape_char") == 0)
|
|
pElem->SetText((int) langmeta.toupper_lower_escape_char);
|
|
else if (SDL_strcmp(pKey, "menu_select") == 0)
|
|
pElem->SetText(langmeta.menu_select.c_str());
|
|
else if (SDL_strcmp(pKey, "menu_select_tight") == 0)
|
|
pElem->SetText(langmeta.menu_select_tight.c_str());
|
|
else if (SDL_strcmp(pKey, "font") == 0)
|
|
pElem->SetText(font::get_main_font_name(langmeta.font_idx));
|
|
}
|
|
|
|
/* This part exists because we want to preserve blank lines between the commented
|
|
* options for clarity, so we have to take matters into our own hands. */
|
|
for (
|
|
tinyxml2::XMLNode* pNode = hDoc.FirstChildElement().FirstChild().ToNode();
|
|
pNode != NULL;
|
|
pNode = pNode->NextSibling()
|
|
)
|
|
{
|
|
tinyxml2::XMLComment* pCom = pNode->ToComment();
|
|
if (pCom != NULL)
|
|
{
|
|
tinyxml2::XMLNode* pPrevNode = pCom->PreviousSibling();
|
|
if (pPrevNode != NULL)
|
|
{
|
|
doc.FirstChildElement()->InsertAfterChild(pPrevNode, doc.NewText("\n\n "));
|
|
}
|
|
doc.FirstChildElement()->InsertAfterChild(pCom, doc.NewText("\n "));
|
|
}
|
|
}
|
|
|
|
FILESYSTEM_saveTiXml2Document((langcode + "/meta.xml").c_str(), doc);
|
|
}
|
|
|
|
if (load_lang_doc("strings", doc, "en"))
|
|
{
|
|
if (max_local_needed)
|
|
{
|
|
write_max_local_decl(&doc, glyph_w, glyph_h);
|
|
}
|
|
|
|
FOR_EACH_XML_ELEMENT(hDoc, pElem)
|
|
{
|
|
EXPECT_ELEM(pElem, "string");
|
|
|
|
const char* eng = pElem->Attribute("english");
|
|
if (eng != NULL)
|
|
{
|
|
char textcase = pElem->UnsignedAttribute("case", 0);
|
|
const char* tra;
|
|
if (textcase == 0)
|
|
{
|
|
tra = map_lookup_text(map_translation, eng, "");
|
|
}
|
|
else
|
|
{
|
|
char* eng_prefixed = add_disambiguator(textcase, eng, NULL);
|
|
if (eng_prefixed == NULL)
|
|
{
|
|
/* Are we out of memory? Stop, don't blank our language files... */
|
|
return;
|
|
}
|
|
/* Note the fallback: if this string used to not be cased and now it is,
|
|
* simply fill in the old single variant we already had. */
|
|
tra = map_lookup_text(
|
|
map_translation,
|
|
eng_prefixed,
|
|
map_lookup_text(map_translation, eng, "")
|
|
);
|
|
VVV_free(eng_prefixed);
|
|
}
|
|
|
|
pElem->SetAttribute("translation", tra);
|
|
}
|
|
|
|
if (max_local_needed)
|
|
{
|
|
write_max_local(pElem, glyph_w, glyph_h);
|
|
}
|
|
}
|
|
|
|
FILESYSTEM_saveTiXml2Document((langcode + "/strings.xml").c_str(), doc);
|
|
}
|
|
|
|
if (load_lang_doc("strings_plural", doc, "en"))
|
|
{
|
|
/* Form 255 is technically invalid, but we have to account for it */
|
|
bool form_id_used[256];
|
|
SDL_zeroa(form_id_used);
|
|
for (int num = 0; num < 200; num++)
|
|
{
|
|
form_id_used[number_plural_form[num]] = true;
|
|
}
|
|
|
|
if (max_local_needed)
|
|
{
|
|
write_max_local_decl(&doc, glyph_w, glyph_h);
|
|
}
|
|
|
|
FOR_EACH_XML_ELEMENT(hDoc, pElem)
|
|
{
|
|
EXPECT_ELEM(pElem, "string");
|
|
|
|
if (max_local_needed)
|
|
{
|
|
write_max_local(pElem, glyph_w, glyph_h);
|
|
}
|
|
|
|
pElem->DeleteChildren();
|
|
|
|
const char* eng_plural = pElem->Attribute("english_plural");
|
|
|
|
for (int form_id = 0; form_id < 255; form_id++)
|
|
{
|
|
if (form_id_used[form_id] && eng_plural != NULL)
|
|
{
|
|
subElem = doc.NewElement("translation");
|
|
pElem->LinkEndChild(subElem);
|
|
|
|
subElem->SetAttribute("form", form_id);
|
|
|
|
char* key = add_disambiguator(form_id+1, eng_plural, NULL);
|
|
if (key == NULL)
|
|
{
|
|
/* Out of memory or something, stop */
|
|
return;
|
|
}
|
|
|
|
subElem->SetAttribute("translation", map_lookup_text(map_translation_plural, key, ""));
|
|
|
|
VVV_free(key);
|
|
}
|
|
}
|
|
}
|
|
|
|
FILESYSTEM_saveTiXml2Document((langcode + "/strings_plural.xml").c_str(), doc);
|
|
}
|
|
|
|
if (load_lang_doc("cutscenes", doc, "en"))
|
|
{
|
|
FOR_EACH_XML_ELEMENT(hDoc, pElem)
|
|
{
|
|
EXPECT_ELEM(pElem, "cutscene");
|
|
|
|
const char* cutscene_id = pElem->Attribute("id");
|
|
if (cutscene_id == NULL)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
hashmap* map = map_translation_cutscene;
|
|
|
|
uintptr_t ptr_cutscene_map;
|
|
bool found = hashmap_get(map, cutscene_id, SDL_strlen(cutscene_id), &ptr_cutscene_map);
|
|
hashmap* cutscene_map = (hashmap*) ptr_cutscene_map;
|
|
if (!found || cutscene_map == NULL)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
FOR_EACH_XML_SUB_ELEMENT(pElem, subElem)
|
|
{
|
|
EXPECT_ELEM(subElem, "dialogue");
|
|
|
|
const char* eng = subElem->Attribute("english");
|
|
if (eng == NULL)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
size_t alloc_len;
|
|
const std::string eng_unwrapped = font::string_unwordwrap(eng);
|
|
char* eng_prefixed = add_disambiguator(subElem->UnsignedAttribute("case", 1), eng_unwrapped.c_str(), &alloc_len);
|
|
if (eng_prefixed == NULL)
|
|
{
|
|
/* Out of memory or something, stop */
|
|
return;
|
|
}
|
|
|
|
uintptr_t ptr_format;
|
|
found = hashmap_get(cutscene_map, eng_prefixed, alloc_len-1, &ptr_format);
|
|
const TextboxFormat* format = (TextboxFormat*) ptr_format;
|
|
|
|
VVV_free(eng_prefixed);
|
|
|
|
if (!found || format == NULL)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
subElem->DeleteAttribute("tt");
|
|
subElem->DeleteAttribute("wraplimit");
|
|
subElem->DeleteAttribute("centertext");
|
|
subElem->DeleteAttribute("pad");
|
|
subElem->DeleteAttribute("pad_left");
|
|
subElem->DeleteAttribute("pad_right");
|
|
subElem->DeleteAttribute("padtowidth");
|
|
|
|
bool buttons = subElem->BoolAttribute("buttons", false);
|
|
subElem->DeleteAttribute("buttons"); // we want this at the end...
|
|
|
|
if (format->text != NULL)
|
|
subElem->SetAttribute("translation", format->text);
|
|
if (format->tt)
|
|
subElem->SetAttribute("tt", 1);
|
|
if (format->wraplimit_raw != 0)
|
|
subElem->SetAttribute("wraplimit", format->wraplimit_raw);
|
|
if (format->centertext)
|
|
subElem->SetAttribute("centertext", 1);
|
|
if (format->pad_left == format->pad_right && format->pad_left != 0)
|
|
{
|
|
subElem->SetAttribute("pad", format->pad_left);
|
|
}
|
|
else
|
|
{
|
|
if (format->pad_left != 0)
|
|
subElem->SetAttribute("pad_left", format->pad_left);
|
|
if (format->pad_right != 0)
|
|
subElem->SetAttribute("pad_right", format->pad_right);
|
|
}
|
|
if (format->padtowidth != 0)
|
|
subElem->SetAttribute("padtowidth", format->padtowidth);
|
|
if (buttons)
|
|
subElem->SetAttribute("buttons", 1);
|
|
}
|
|
}
|
|
|
|
FILESYSTEM_saveTiXml2Document((langcode + "/cutscenes.xml").c_str(), doc);
|
|
}
|
|
|
|
if (load_lang_doc("roomnames", doc, "en"))
|
|
{
|
|
FOR_EACH_XML_ELEMENT(hDoc, pElem)
|
|
{
|
|
EXPECT_ELEM(pElem, "roomname");
|
|
|
|
pElem->SetAttribute("translation",
|
|
get_roomname_translation(false, pElem->UnsignedAttribute("x"), pElem->UnsignedAttribute("y"))
|
|
);
|
|
}
|
|
|
|
FILESYSTEM_saveTiXml2Document((langcode + "/roomnames.xml").c_str(), doc);
|
|
}
|
|
|
|
if (load_lang_doc("roomnames_special", doc, "en"))
|
|
{
|
|
FOR_EACH_XML_ELEMENT(hDoc, pElem)
|
|
{
|
|
EXPECT_ELEM(pElem, "roomname");
|
|
|
|
const char* eng = pElem->Attribute("english");
|
|
if (eng != NULL)
|
|
{
|
|
pElem->SetAttribute("translation", map_lookup_text(map_translation_roomnames_special, eng, ""));
|
|
}
|
|
}
|
|
|
|
FILESYSTEM_saveTiXml2Document((langcode + "/roomnames_special.xml").c_str(), doc);
|
|
}
|
|
}
|
|
|
|
bool sync_lang_files(void)
|
|
{
|
|
/* Returns false if we can't set the lang write dir, true otherwise.
|
|
* This could maybe be extended with better error reporting,
|
|
* problem is getting across which files failed in which languages. */
|
|
std::string oldlang = lang;
|
|
if (!FILESYSTEM_setLangWriteDir())
|
|
{
|
|
vlog_error("Cannot set write dir to lang dir, not syncing language files");
|
|
return false;
|
|
}
|
|
|
|
for (size_t i = 0; i < languagelist.size(); i++)
|
|
{
|
|
if (languagelist[i].code != "en")
|
|
sync_lang_file(languagelist[i].code);
|
|
}
|
|
|
|
FILESYSTEM_restoreWriteDir();
|
|
lang = oldlang;
|
|
loadtext(false);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool save_roomname_to_file(const std::string& langcode, bool custom_level, int roomx, int roomy, const char* tra, const char* explanation)
|
|
{
|
|
if (custom_level)
|
|
{
|
|
vlog_error("Saving custom level room names not implemented");
|
|
return false;
|
|
}
|
|
|
|
if (!fix_room_coords(custom_level, &roomx, &roomy))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
tinyxml2::XMLDocument doc;
|
|
tinyxml2::XMLHandle hDoc(&doc);
|
|
tinyxml2::XMLElement* pElem;
|
|
|
|
if (!load_lang_doc("roomnames", doc, langcode))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool found = false;
|
|
FOR_EACH_XML_ELEMENT(hDoc, pElem)
|
|
{
|
|
EXPECT_ELEM(pElem, "roomname");
|
|
|
|
int x = pElem->IntAttribute("x", -1);
|
|
int y = pElem->IntAttribute("y", -1);
|
|
|
|
if (x == roomx && y == roomy)
|
|
{
|
|
if (explanation != NULL)
|
|
{
|
|
pElem->SetAttribute("explanation", explanation);
|
|
}
|
|
if (tra != NULL)
|
|
{
|
|
pElem->SetAttribute("translation", tra);
|
|
}
|
|
found = true;
|
|
}
|
|
}
|
|
|
|
if (!found)
|
|
{
|
|
vlog_error("Could not find room %d,%d in language file to replace!", roomx, roomy);
|
|
return false;
|
|
}
|
|
|
|
if (!FILESYSTEM_setLangWriteDir())
|
|
{
|
|
vlog_error("Cannot set write dir to lang dir, so room name can't be saved");
|
|
return false;
|
|
}
|
|
bool save_success = FILESYSTEM_saveTiXml2Document((langcode + "/roomnames.xml").c_str(), doc);
|
|
FILESYSTEM_restoreWriteDir();
|
|
if (!save_success)
|
|
{
|
|
vlog_error("Could not write roomnames document!");
|
|
return false;
|
|
}
|
|
return store_roomname_translation(custom_level, roomx, roomy, tra, explanation);
|
|
}
|
|
|
|
bool save_roomname_explanation_to_files(bool custom_level, int roomx, int roomy, const char* explanation)
|
|
{
|
|
bool success = true;
|
|
for (size_t i = 0; i < languagelist.size(); i++)
|
|
{
|
|
if (!save_roomname_to_file(languagelist[i].code, custom_level, roomx, roomy, NULL, explanation))
|
|
{
|
|
success = false;
|
|
vlog_warn("Could not save room name explanation to language %s", languagelist[i].code.c_str());
|
|
}
|
|
}
|
|
|
|
return !languagelist.empty() && success;
|
|
}
|
|
|
|
void local_limits_check(void)
|
|
{
|
|
text_overflows.clear();
|
|
loadtext(true);
|
|
limitscheck_current_overflow = 0;
|
|
}
|
|
|
|
void global_limits_check(void)
|
|
{
|
|
text_overflows.clear();
|
|
|
|
std::string oldlang = lang;
|
|
|
|
textbook_clear(&textbook_main);
|
|
textbook_set_protected(&textbook_main, true);
|
|
|
|
for (size_t i = 0; i < languagelist.size(); i++)
|
|
{
|
|
if (languagelist[i].code != "en")
|
|
{
|
|
lang = languagelist[i].code;
|
|
loadtext(true);
|
|
}
|
|
}
|
|
|
|
lang = oldlang;
|
|
loadtext(false);
|
|
|
|
textbook_set_protected(&textbook_main, false);
|
|
|
|
limitscheck_current_overflow = 0;
|
|
}
|
|
|
|
void populate_testable_script_ids(void)
|
|
{
|
|
testable_script_ids.clear();
|
|
|
|
tinyxml2::XMLDocument doc;
|
|
tinyxml2::XMLHandle hDoc(&doc);
|
|
tinyxml2::XMLElement* pElem;
|
|
|
|
if (!load_lang_doc("cutscenes", doc))
|
|
{
|
|
return;
|
|
}
|
|
|
|
FOR_EACH_XML_ELEMENT(hDoc, pElem)
|
|
{
|
|
EXPECT_ELEM(pElem, "cutscene");
|
|
|
|
const char* id = pElem->Attribute("id");
|
|
if (id != NULL)
|
|
{
|
|
testable_script_ids.push_back(id);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool populate_cutscene_test(const char* script_id)
|
|
{
|
|
tinyxml2::XMLDocument doc;
|
|
tinyxml2::XMLHandle hDoc(&doc);
|
|
tinyxml2::XMLElement* pElem;
|
|
|
|
if (!load_lang_doc("cutscenes", doc))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
const char* original = get_level_original_lang(hDoc);
|
|
|
|
FOR_EACH_XML_ELEMENT(hDoc, pElem)
|
|
{
|
|
EXPECT_ELEM(pElem, "cutscene");
|
|
|
|
if (SDL_strcmp(pElem->Attribute("id"), script_id) != 0)
|
|
{
|
|
/* Not the correct cutscene */
|
|
continue;
|
|
}
|
|
|
|
tinyxml2::XMLElement* subElem;
|
|
FOR_EACH_XML_SUB_ELEMENT(pElem, subElem)
|
|
{
|
|
EXPECT_ELEM(subElem, "dialogue");
|
|
|
|
const char* tra = subElem->Attribute("translation");
|
|
const char* speaker = subElem->Attribute("speaker");
|
|
const char* eng = subElem->Attribute(original);
|
|
if (tra != NULL && tra[0] != '\0' && speaker != NULL && eng != NULL)
|
|
{
|
|
script.add_test_line(
|
|
speaker,
|
|
eng,
|
|
subElem->UnsignedAttribute("case", 1),
|
|
subElem->BoolAttribute("buttons", false)
|
|
);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
} /* namespace loc */
|