From ec611ffa9dc8ccc44d94f52ce2a25541897f1a8a Mon Sep 17 00:00:00 2001 From: Dav999-v Date: Fri, 30 Dec 2022 22:57:24 +0100 Subject: [PATCH] Add localization "foundation" (many code changes) This commit adds most of the code changes necessary for making the game translatable, but does not yet "unhardcode" nearly all of the strings (except in a few cases where it was hard to separate added loc::gettexts from foundational code changes, or all the localization- related menus which were also added by this commit.) This commit is part of rewritten history of the localization branch. The original (unsquashed) commit history can be found here: https://github.com/Dav999-v/VVVVVV/tree/localization-orig --- desktop_version/CMakeLists.txt | 4 + desktop_version/src/CustomLevels.cpp | 27 +- desktop_version/src/CustomLevels.h | 4 + desktop_version/src/Editor.cpp | 2 +- desktop_version/src/FileSystemUtils.cpp | 236 +++++++++++++++- desktop_version/src/FileSystemUtils.h | 13 +- desktop_version/src/Game.cpp | 127 ++++++++- desktop_version/src/Game.h | 16 +- desktop_version/src/Graphics.cpp | 332 ++++++++++++++++++++-- desktop_version/src/Graphics.h | 32 ++- desktop_version/src/Input.cpp | 356 ++++++++++++++++++++++-- desktop_version/src/KeyPoll.cpp | 19 ++ desktop_version/src/KeyPoll.h | 2 + desktop_version/src/Logic.cpp | 5 + desktop_version/src/Render.cpp | 229 +++++++++++++-- desktop_version/src/Script.cpp | 132 ++++++++- desktop_version/src/Script.h | 7 + desktop_version/src/Scripts.cpp | 1 + desktop_version/src/Textbox.cpp | 43 ++- desktop_version/src/Textbox.h | 6 + desktop_version/src/XMLUtils.h | 28 ++ desktop_version/src/main.cpp | 41 ++- 22 files changed, 1567 insertions(+), 95 deletions(-) diff --git a/desktop_version/CMakeLists.txt b/desktop_version/CMakeLists.txt index 1e7af147..9edcbfbe 100644 --- a/desktop_version/CMakeLists.txt +++ b/desktop_version/CMakeLists.txt @@ -83,6 +83,9 @@ set(VVV_SRC src/Input.cpp src/KeyPoll.cpp src/Labclass.cpp + src/Localization.cpp + src/LocalizationMaint.cpp + src/LocalizationStorage.cpp src/Logic.cpp src/Map.cpp src/Music.cpp @@ -90,6 +93,7 @@ set(VVV_SRC src/preloader.cpp src/Render.cpp src/RenderFixed.cpp + src/RoomnameTranslator.cpp src/Screen.cpp src/Script.cpp src/Scripts.cpp diff --git a/desktop_version/src/CustomLevels.cpp b/desktop_version/src/CustomLevels.cpp index ff03132c..fa0ebb63 100644 --- a/desktop_version/src/CustomLevels.cpp +++ b/desktop_version/src/CustomLevels.cpp @@ -18,6 +18,8 @@ #include "Graphics.h" #include "GraphicsUtil.h" #include "KeyPoll.h" +#include "Localization.h" +#include "LocalizationStorage.h" #include "Map.h" #include "Script.h" #include "UtilityClass.h" @@ -81,6 +83,24 @@ static bool compare_nocase (std::string first, std::string second) return false; } +/* translate_title and translate_creator are used to display default title/author + * as being translated, while they're actually stored in English in the level file. + * This way we translate "Untitled Level" and "Unknown" without + * spreading around translations in level files posted online! */ +std::string translate_title(const std::string& title) +{ + if (title == "Untitled Level") + return loc::gettext("Untitled Level"); + return title; +} + +std::string translate_creator(const std::string& creator) +{ + if (creator == "Unknown") + return loc::gettext("Unknown"); + return creator; +} + static void levelZipCallback(const char* filename) { if (!FILESYSTEM_isFile(filename)) @@ -228,6 +248,9 @@ static void levelMetaDataCallback(const char* filename) if (cl.getLevelMetaData(filename_, temp)) { + temp.title = translate_title(temp.title); + temp.creator = translate_creator(temp.creator); + cl.ListOfMetaData.push_back(temp); } } @@ -318,7 +341,7 @@ void customlevelclass::reset(void) mapwidth=5; mapheight=5; - title="Untitled Level"; + title="Untitled Level"; // Already translatable creator="Unknown"; levmusic=0; @@ -1288,6 +1311,8 @@ next: ed.gethooks(); #endif + loc::loadtext_custom(_path.c_str()); + version=2; return true; diff --git a/desktop_version/src/CustomLevels.h b/desktop_version/src/CustomLevels.h index a795f746..d08b7ac7 100644 --- a/desktop_version/src/CustomLevels.h +++ b/desktop_version/src/CustomLevels.h @@ -160,6 +160,10 @@ public: bool onewaycol_override; }; +std::string translate_title(const std::string& title); + +std::string translate_creator(const std::string& creator); + #ifndef CL_DEFINITION extern customlevelclass cl; #endif diff --git a/desktop_version/src/Editor.cpp b/desktop_version/src/Editor.cpp index cbf14c69..7185ad23 100644 --- a/desktop_version/src/Editor.cpp +++ b/desktop_version/src/Editor.cpp @@ -1220,7 +1220,7 @@ void editorrender(void) if(tb>255) tb=255; editormenurender(tr, tg, tb); - graphics.drawmenu(tr, tg, tb); + graphics.drawmenu(tr, tg, tb, game.currentmenuname); } else if (ed.textmod) { diff --git a/desktop_version/src/FileSystemUtils.cpp b/desktop_version/src/FileSystemUtils.cpp index d9402882..60ce9cd5 100644 --- a/desktop_version/src/FileSystemUtils.cpp +++ b/desktop_version/src/FileSystemUtils.cpp @@ -6,12 +6,15 @@ #include "Alloc.h" #include "BinaryBlob.h" +#include "Constants.h" #include "Exit.h" #include "Graphics.h" +#include "Localization.h" #include "Maths.h" #include "Screen.h" #include "Unused.h" #include "UtilityClass.h" +#include "VFormat.h" #include "Vlogging.h" /* These are needed for PLATFORM_* crap */ @@ -40,8 +43,11 @@ static bool isInit = false; static const char* pathSep = NULL; static char* basePath = NULL; +static char writeDir[MAX_PATH] = {'\0'}; static char saveDir[MAX_PATH] = {'\0'}; static char levelDir[MAX_PATH] = {'\0'}; +static char mainLangDir[MAX_PATH] = {'\0'}; +static bool isMainLangDirFromRepo = false; static char assetDir[MAX_PATH] = {'\0'}; static char virtualMountPath[MAX_PATH] = {'\0'}; @@ -66,7 +72,105 @@ static const PHYSFS_Allocator allocator = { SDL_free }; -int FILESYSTEM_init(char *argvZero, char* baseDir, char *assetsPath) +void mount_pre_datazip( + char* out_path, + const char* real_dirname, + const char* mount_point, + const char* user_path +) +{ + /* Find and mount a directory (like the main language directory) in front of data.zip. + * This directory, if not user-supplied, can be either next to data.zip, + * or otherwise in desktop_version/ if that's found in the base path. + * + * out_path is assumed to be either NULL, or MAX_PATH long. If it isn't, boom */ + + if (user_path != NULL) + { + if (PHYSFS_mount(user_path, mount_point, 1)) + { + if (out_path != NULL) + { + SDL_strlcpy(out_path, user_path, MAX_PATH); + } + } + else + { + vlog_warn("User-supplied %s directory is invalid!", real_dirname); + } + return; + } + + /* Try to detect the directory, it's next to data.zip in distributed builds */ + bool dir_found = false; + char buffer[MAX_PATH]; + + SDL_snprintf(buffer, sizeof(buffer), "%s%s%s", + basePath, + real_dirname, + pathSep + ); + if (PHYSFS_mount(buffer, mount_point, 1)) + { + dir_found = true; + } + else + { + /* If you're a developer, you probably want to use the language files/fonts + * from the repo, otherwise it's a pain to keep everything in sync. + * And who knows how deep in build folders our binary is. */ + size_t buf_reserve = SDL_strlen(real_dirname)+1; + SDL_strlcpy(buffer, basePath, sizeof(buffer)-buf_reserve); + + char needle[32]; + SDL_snprintf(needle, sizeof(needle), "%sdesktop_version%s", + pathSep, + pathSep + ); + + /* We want the last match */ + char* match_last = NULL; + char* match = buffer; + while ((match = SDL_strstr(match, needle))) + { + match_last = match; + match = &match[1]; + } + + if (match_last != NULL) + { + /* strstr only gives us a pointer and not a remaining buffer length, but that's + * why we pretended the buffer was `buf_reserve` chars shorter than it was! */ + SDL_strlcpy(&match_last[SDL_strlen(needle)], real_dirname, buf_reserve); + SDL_strlcat(buffer, pathSep, sizeof(buffer)); + + if (PHYSFS_mount(buffer, mount_point, 1)) + { + dir_found = true; + + if (SDL_strcmp(real_dirname, "lang") == 0) + { + loc::show_translator_menu = true; + isMainLangDirFromRepo = true; + } + } + } + } + + if (dir_found) + { + if (out_path != NULL) + { + SDL_strlcpy(out_path, buffer, MAX_PATH); + } + } + else + { + vlog_warn("Cannot find the %s directory anywhere!", real_dirname); + } +} + +int FILESYSTEM_init(char *argvZero, char* baseDir, char *assetsPath, char* langDir, char* fontsDir) { char output[MAX_PATH]; @@ -102,29 +206,30 @@ int FILESYSTEM_init(char *argvZero, char* baseDir, char *assetsPath) } /* Mount our base user directory */ - if (!PHYSFS_mount(output, NULL, 0)) + SDL_strlcpy(writeDir, output, sizeof(writeDir)); + if (!PHYSFS_mount(writeDir, NULL, 0)) { vlog_error( "Could not mount %s: %s", - output, + writeDir, PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode()) ); return 0; } - if (!PHYSFS_setWriteDir(output)) + if (!PHYSFS_setWriteDir(writeDir)) { vlog_error( "Could not set write dir to %s: %s", - output, + writeDir, PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode()) ); return 0; } - vlog_info("Base directory: %s", output); + vlog_info("Base directory: %s", writeDir); /* Store full save directory */ SDL_snprintf(saveDir, sizeof(saveDir), "%s%s%s", - output, + writeDir, "saves", pathSep ); @@ -133,7 +238,7 @@ int FILESYSTEM_init(char *argvZero, char* baseDir, char *assetsPath) /* Store full level directory */ SDL_snprintf(levelDir, sizeof(levelDir), "%s%s%s", - output, + writeDir, "levels", pathSep ); @@ -148,6 +253,11 @@ int FILESYSTEM_init(char *argvZero, char* baseDir, char *assetsPath) basePath = SDL_strdup("./"); } + mount_pre_datazip(mainLangDir, "lang", "lang/", langDir); + vlog_info("Languages directory: %s", mainLangDir); + + mount_pre_datazip(NULL, "fonts", "graphics/", fontsDir); + /* Mount the stock content last */ if (assetsPath) { @@ -217,7 +327,40 @@ char *FILESYSTEM_getUserLevelDirectory(void) return levelDir; } -bool FILESYSTEM_isFile(const char* filename) +char *FILESYSTEM_getUserMainLangDirectory(void) +{ + return mainLangDir; +} + +bool FILESYSTEM_isMainLangDirFromRepo(void) +{ + return isMainLangDirFromRepo; +} + +bool FILESYSTEM_restoreWriteDir(void) +{ + return PHYSFS_setWriteDir(writeDir); +} + +bool FILESYSTEM_setLangWriteDir(void) +{ + const char* realLangDir = PHYSFS_getRealDir("lang"); + if (realLangDir == NULL || SDL_strcmp(mainLangDir, realLangDir) != 0) + { + vlog_error("Not setting language write dir: %s overrules %s when loading", + realLangDir, mainLangDir + ); + return false; + } + if (!PHYSFS_setWriteDir(mainLangDir)) + { + FILESYSTEM_restoreWriteDir(); + return false; + } + return true; +} + +bool FILESYSTEM_isFileType(const char* filename, PHYSFS_FileType filetype) { PHYSFS_Stat stat; @@ -235,10 +378,20 @@ bool FILESYSTEM_isFile(const char* filename) /* We unfortunately cannot follow symlinks (PhysFS limitation). * Let the caller deal with them. */ - return stat.filetype == PHYSFS_FILETYPE_REGULAR + return stat.filetype == filetype || stat.filetype == PHYSFS_FILETYPE_SYMLINK; } +bool FILESYSTEM_isFile(const char* filename) +{ + return FILESYSTEM_isFileType(filename, PHYSFS_FILETYPE_REGULAR); +} + +bool FILESYSTEM_isDirectory(const char* filename) +{ + return FILESYSTEM_isFileType(filename, PHYSFS_FILETYPE_DIRECTORY); +} + bool FILESYSTEM_isMounted(const char* filename) { return PHYSFS_getMountPoint(filename) != NULL; @@ -282,7 +435,7 @@ static void generateVirtualMountPath(char* path, const size_t path_size) ); } -static char levelDirError[256] = {'\0'}; +static char levelDirError[6*SCREEN_WIDTH_CHARS + 1] = {'\0'}; static bool levelDirHasError = false; @@ -501,6 +654,31 @@ bool FILESYSTEM_isAssetMounted(const char* filename) return SDL_strcmp(assetDir, realDir) == 0; } +bool FILESYSTEM_areAssetsInSameRealDir(const char* filenameA, const char* filenameB) +{ + char pathA[MAX_PATH]; + char pathB[MAX_PATH]; + + getMountedPath(pathA, sizeof(pathA), filenameA); + getMountedPath(pathB, sizeof(pathB), filenameB); + + const char* realDirA = PHYSFS_getRealDir(pathA); + const char* realDirB = PHYSFS_getRealDir(pathB); + + /* Both NULL, or both the same pointer? */ + if (realDirA == realDirB) + { + return true; + } + + if (realDirA == NULL || realDirB == NULL) + { + return false; + } + + return SDL_strcmp(realDirA, realDirB) == 0; +} + static void load_stdin(void) { size_t pos = 0; @@ -809,6 +987,20 @@ bool FILESYSTEM_loadTiXml2Document(const char *name, tinyxml2::XMLDocument& doc) return true; } +bool FILESYSTEM_loadAssetTiXml2Document(const char *name, tinyxml2::XMLDocument& doc) +{ + /* Same as FILESYSTEM_loadTiXml2Document except for possible custom assets */ + unsigned char *mem; + FILESYSTEM_loadAssetToMemory(name, &mem, NULL, true); + if (mem == NULL) + { + return false; + } + doc.Parse((const char*) mem); + VVV_free(mem); + return true; +} + struct CallbackWrapper { void (*callback)(const char* filename); @@ -853,6 +1045,28 @@ void FILESYSTEM_enumerateLevelDirFileNames( } } +std::vector FILESYSTEM_getLanguageCodes(void) +{ + std::vector list; + char** fileList = PHYSFS_enumerateFiles("lang"); + char** item; + + for (item = fileList; *item != NULL; item++) + { + char fullName[128]; + SDL_snprintf(fullName, sizeof(fullName), "lang/%s", *item); + + if (FILESYSTEM_isDirectory(fullName) && *item[0] != '.') + { + list.push_back(*item); + } + } + + PHYSFS_freeList(fileList); + + return list; +} + static int PLATFORM_getOSDirectory(char* output, const size_t output_size) { #ifdef _WIN32 diff --git a/desktop_version/src/FileSystemUtils.h b/desktop_version/src/FileSystemUtils.h index 7358673b..2701b231 100644 --- a/desktop_version/src/FileSystemUtils.h +++ b/desktop_version/src/FileSystemUtils.h @@ -5,16 +5,23 @@ class binaryBlob; #include +#include +#include // Forward declaration, including the entirety of tinyxml2.h across all files this file is included in is unnecessary namespace tinyxml2 { class XMLDocument; } -int FILESYSTEM_init(char *argvZero, char* baseDir, char* assetsPath); +int FILESYSTEM_init(char *argvZero, char* baseDir, char* assetsPath, char* langDir, char* fontsDir); bool FILESYSTEM_isInit(void); void FILESYSTEM_deinit(void); char *FILESYSTEM_getUserSaveDirectory(void); char *FILESYSTEM_getUserLevelDirectory(void); +char *FILESYSTEM_getUserMainLangDirectory(void); +bool FILESYSTEM_isMainLangDirFromRepo(void); + +bool FILESYSTEM_setLangWriteDir(void); +bool FILESYSTEM_restoreWriteDir(void); bool FILESYSTEM_isFile(const char* filename); bool FILESYSTEM_isMounted(const char* filename); @@ -23,6 +30,7 @@ void FILESYSTEM_loadZip(const char* filename); bool FILESYSTEM_mountAssets(const char *path); void FILESYSTEM_unmountAssets(void); bool FILESYSTEM_isAssetMounted(const char* filename); +bool FILESYSTEM_areAssetsInSameRealDir(const char* filenameA, const char* filenameB); void FILESYSTEM_loadFileToMemory(const char *name, unsigned char **mem, size_t *len, bool addnull); @@ -37,9 +45,12 @@ bool FILESYSTEM_loadBinaryBlob(binaryBlob* blob, const char* filename); bool FILESYSTEM_saveTiXml2Document(const char *name, tinyxml2::XMLDocument& doc, bool sync = true); bool FILESYSTEM_loadTiXml2Document(const char *name, tinyxml2::XMLDocument& doc); +bool FILESYSTEM_loadAssetTiXml2Document(const char *name, tinyxml2::XMLDocument& doc); void FILESYSTEM_enumerateLevelDirFileNames(void (*callback)(const char* filename)); +std::vector FILESYSTEM_getLanguageCodes(void); + bool FILESYSTEM_levelDirHasError(void); void FILESYSTEM_clearLevelDirError(void); const char* FILESYSTEM_getLevelDirError(void); diff --git a/desktop_version/src/Game.cpp b/desktop_version/src/Game.cpp index 24b9647a..06679fb6 100644 --- a/desktop_version/src/Game.cpp +++ b/desktop_version/src/Game.cpp @@ -15,14 +15,18 @@ #include "FileSystemUtils.h" #include "GlitchrunnerMode.h" #include "Graphics.h" +#include "Localization.h" +#include "LocalizationStorage.h" #include "KeyPoll.h" #include "MakeAndPlay.h" #include "Map.h" #include "Music.h" #include "Network.h" +#include "RoomnameTranslator.h" #include "Screen.h" #include "Script.h" #include "UtilityClass.h" +#include "VFormat.h" #include "Vlogging.h" #include "XMLUtils.h" @@ -262,13 +266,13 @@ void Game::init(void) SDL_memset(unlocknotify, false, sizeof(unlock)); currentmenuoption = 0; + menutestmode = false; current_credits_list_index = 0; menuxoff = 0; menuyoff = 0; menucountdown = 0; levelpage=0; playcustomlevel=0; - createmenu(Menu::mainmenu); silence_settings_error = false; @@ -288,6 +292,7 @@ void Game::init(void) timetrialshinytarget = 0; timetrialparlost = false; timetrialpar = 0; + timetrialcheater = false; timetrialresulttime = 0; timetrialresultframes = 0; timetrialresultshinytarget = 0; @@ -1455,6 +1460,11 @@ void Game::updatestate(void) obj.removetrigger(82); hascontrol = false; + if (timetrialcheater) + { + SDL_zeroa(obj.collect); + } + timetrialresulttime = help.hms_to_seconds(hours, minutes, seconds); timetrialresultframes = frames; timetrialresulttrinkets = trinkets(); @@ -4379,6 +4389,21 @@ void Game::deserializesettings(tinyxml2::XMLElement* dataNode, struct ScreenSett key.sensitivity = help.Int(pText); } + if (SDL_strcmp(pKey, "lang") == 0) + { + loc::lang = std::string(pText); + } + + if (SDL_strcmp(pKey, "lang_set") == 0) + { + loc::lang_set = help.Int(pText); + } + + if (SDL_strcmp(pKey, "roomname_translator") == 0 && loc::show_translator_menu) + { + roomname_translator::set_enabled(help.Int(pText)); + } + } if (controllerButton_flip.size() < 1) @@ -4648,6 +4673,10 @@ void Game::serializesettings(tinyxml2::XMLElement* dataNode, const struct Screen } xml::update_tag(dataNode, "controllerSensitivity", key.sensitivity); + + xml::update_tag(dataNode, "lang", loc::lang.c_str()); + xml::update_tag(dataNode, "lang_set", (int) loc::lang_set); + xml::update_tag(dataNode, "roomname_translator", (int) roomname_translator::enabled); } static bool settings_loaded = false; @@ -5281,6 +5310,14 @@ void Game::customloadquick(const std::string& savfile) music.play(song); } } + else if (SDL_strcmp(pKey, "lang_custom") == 0) + { + loc::lang_custom = pText; + if (pText[0] != '\0') + { + loc::loadtext_custom(NULL); + } + } else if (SDL_strcmp(pKey, "showminimap") == 0) { map.customshowmm = help.Int(pText); @@ -5738,6 +5775,8 @@ bool Game::customsavequick(const std::string& savfile) xml::update_tag(msgs, "currentsong", music.currentsong); } + xml::update_tag(msgs, "lang_custom", loc::lang_custom.c_str()); + xml::update_tag(msgs, "teleportscript", teleportscript.c_str()); xml::update_tag(msgs, "companion", companion); @@ -5792,6 +5831,7 @@ void Game::loadtele(void) std::string Game::unrescued(void) { //Randomly return the name of an unrescued crewmate + //Localization is handled with regular cutscene dialogue if (fRandom() * 100 > 50) { if (!crewstats[5]) return "Victoria"; @@ -5933,7 +5973,7 @@ void Game::returntomenu(enum Menu::MenuName t) void Game::createmenu( enum Menu::MenuName t, bool samemenu/*= false*/ ) { - if (t == Menu::mainmenu) + if (t == Menu::mainmenu && !menutestmode) { //Either we've just booted up the game or returned from gamemode //Whichever it is, we shouldn't have a stack, @@ -5971,6 +6011,10 @@ void Game::createmenu( enum Menu::MenuName t, bool samemenu/*= false*/ ) option("levels"); #endif option("options"); + if (loc::show_translator_menu) + { + option(loc::gettext("translator")); + } #if !defined(MAKEANDPLAY) option("credits"); #endif @@ -6259,6 +6303,66 @@ void Game::createmenu( enum Menu::MenuName t, bool samemenu/*= false*/ ) menuyoff = 0; maxspacing = 10; break; + case Menu::language: + if (loc::languagelist.empty()) + { + option(loc::gettext("ok")); + menuyoff = -20; + } + else + { + for (size_t i = 0; i < loc::languagelist.size(); i++) + { + if (loc::languagelist[i].nativename.empty()) + option(loc::languagelist[i].code.c_str()); + else + option(loc::languagelist[i].nativename.c_str()); + } + + menuyoff = 70-(menuoptions.size()*10); + maxspacing = 5; + } + break; + case Menu::translator_main: + option(loc::gettext("translator options")); + option(loc::gettext("maintenance")); + option(loc::gettext("open lang folder"), FILESYSTEM_openDirectoryEnabled()); + option(loc::gettext("return")); + menuyoff = 0; + break; + case Menu::translator_options: + option(loc::gettext("language statistics")); + option(loc::gettext("translate room names")); + option(loc::gettext("menu test")); + option(loc::gettext("limits check")); + option(loc::gettext("return")); + menuyoff = 0; + break; + case Menu::translator_options_limitscheck: + option(loc::gettext("next page")); + option(loc::gettext("return")); + menuyoff = 64; + break; + case Menu::translator_options_stats: + option(loc::gettext("return")); + menuyoff = 64; + break; + case Menu::translator_maintenance: + option(loc::gettext("sync language files")); + option(loc::gettext("global statistics"), false); + option(loc::gettext("global limits check")); + option(loc::gettext("return")); + menuyoff = 0; + break; + case Menu::translator_maintenance_sync: + option(loc::gettext("sync")); + option(loc::gettext("return")); + menuyoff = 64; + break; + case Menu::translator_error_setlangwritedir: + option(loc::gettext("ok")); + menuyoff = 10; + break; case Menu::cleardatamenu: case Menu::clearcustomdatamenu: option("no! don't delete"); @@ -6773,6 +6877,7 @@ void Game::quittomenu(void) gamestate = TITLEMODE; graphics.fademode = FADE_START_FADEIN; FILESYSTEM_unmountAssets(); + loc::unloadtext_custom(); cliplaytest = false; graphics.titlebg.tdrawback = true; graphics.flipmode = false; @@ -7015,6 +7120,11 @@ int Game::get_timestep(void) } } +bool Game::physics_frozen(void) +{ + return roomname_translator::is_pausing(); +} + bool Game::incompetitive(void) { return ( @@ -7031,6 +7141,19 @@ bool Game::nocompetitive(void) return slowdown < 30 || map.invincibility; } +bool Game::nocompetitive_unless_translator(void) +{ + return slowdown < 30 || (map.invincibility && !roomname_translator::enabled); +} + +void Game::sabotage_time_trial(void) +{ + timetrialcheater = true; + hours++; + deathcounts += 100; + timetrialparlost = true; +} + bool Game::isingamecompletescreen() { return (state >= 3501 && state <= 3518) || (state >= 3520 && state <= 3522); diff --git a/desktop_version/src/Game.h b/desktop_version/src/Game.h index 7c14a278..55adf16b 100644 --- a/desktop_version/src/Game.h +++ b/desktop_version/src/Game.h @@ -60,6 +60,14 @@ namespace Menu audiooptions, accessibility, controller, + language, + translator_main, + translator_options, + translator_options_limitscheck, + translator_options_stats, + translator_maintenance, + translator_maintenance_sync, + translator_error_setlangwritedir, cleardatamenu, clearcustomdatamenu, setinvincibility, @@ -290,6 +298,7 @@ public: //Main Menu Variables std::vector menuoptions; int currentmenuoption ; + bool menutestmode; enum Menu::MenuName currentmenuname; enum Menu::MenuName kludge_ingametemp; enum SLIDERMODE slidermode; @@ -330,6 +339,7 @@ public: bool noflashingmode; int slowdown; int get_timestep(void); + bool physics_frozen(void); bool nodeathmode; int gameoverdelay; @@ -343,6 +353,7 @@ public: bool intimetrial, timetrialparlost; int timetrialcountdown, timetrialshinytarget, timetriallevel; int timetrialpar, timetrialresulttime, timetrialresultframes, timetrialrank; + bool timetrialcheater; int timetrialresultshinytarget, timetrialresulttrinkets, timetrialresultpar; int timetrialresultdeaths; @@ -423,7 +434,7 @@ public: //Some stats: int totalflips; - std::string hardestroom; + std::string hardestroom; // don't change to C string unless you wanna handle language switches (or make it store coords) int hardestroomdeaths, currentroomdeaths; @@ -483,6 +494,9 @@ public: bool incompetitive(void); bool nocompetitive(void); + bool nocompetitive_unless_translator(void); + + void sabotage_time_trial(void); bool over30mode; bool showingametimer; diff --git a/desktop_version/src/Graphics.cpp b/desktop_version/src/Graphics.cpp index 8bf44e0c..72be7221 100644 --- a/desktop_version/src/Graphics.cpp +++ b/desktop_version/src/Graphics.cpp @@ -11,10 +11,13 @@ #include "Exit.h" #include "FileSystemUtils.h" #include "GraphicsUtil.h" +#include "Localization.h" #include "Map.h" #include "Music.h" +#include "RoomnameTranslator.h" #include "Screen.h" #include "UtilityClass.h" +#include "VFormat.h" #include "Vlogging.h" void Graphics::init(void) @@ -150,6 +153,11 @@ void Graphics::init(void) minimap_mounted = false; #endif + gamecomplete_mounted = false; + levelcomplete_mounted = false; + flipgamecomplete_mounted = false; + fliplevelcomplete_mounted = false; + SDL_zeroa(error); SDL_zeroa(error_title); } @@ -193,6 +201,11 @@ void Graphics::create_buffers(const SDL_PixelFormat* fmt) SDL_SetSurfaceAlphaMod(footerbuffer, 127); FillRect(footerbuffer, SDL_MapRGB(fmt, 0, 0, 0)); + roomname_translator::dimbuffer = CREATE_SURFACE(320, 240); + SDL_SetSurfaceBlendMode(roomname_translator::dimbuffer, SDL_BLENDMODE_BLEND); + SDL_SetSurfaceAlphaMod(roomname_translator::dimbuffer, 96); + FillRect(roomname_translator::dimbuffer, SDL_MapRGB(fmt, 0, 0, 0)); + ghostbuffer = CREATE_SURFACE(320, 240); SDL_SetSurfaceBlendMode(ghostbuffer, SDL_BLENDMODE_BLEND); SDL_SetSurfaceAlphaMod(ghostbuffer, 127); @@ -233,6 +246,7 @@ void Graphics::destroy_buffers(void) FREE_SURFACE(backBuffer); FREE_SURFACE(footerbuffer); + FREE_SURFACE(roomname_translator::dimbuffer); FREE_SURFACE(ghostbuffer); FREE_SURFACE(foregroundBuffer); FREE_SURFACE(menubuffer); @@ -360,9 +374,12 @@ bool Graphics::Makebfont(void) flipbfont.push_back(TempFlipped); }) - unsigned char* charmap; + unsigned char* charmap = NULL; size_t length; - FILESYSTEM_loadAssetToMemory("graphics/font.txt", &charmap, &length, false); + if (FILESYSTEM_areAssetsInSameRealDir("graphics/font.png", "graphics/font.txt")) + { + FILESYSTEM_loadAssetToMemory("graphics/font.txt", &charmap, &length, false); + } if (charmap != NULL) { unsigned char* current = charmap; @@ -584,10 +601,14 @@ bool Graphics::next_wrap( switch (str[idx]) { case ' ': - lenfromlastspace = idx; - lastspace = *start; + if (loc::get_langmeta()->autowordwrap) + { + lenfromlastspace = idx; + lastspace = *start; + } break; case '\n': + case '|': *start += 1; SDL_FALLTHROUGH; case '\0': @@ -634,17 +655,29 @@ bool Graphics::next_wrap_s( return retval; } -void Graphics::PrintWrap( +int Graphics::PrintWrap( const int x, int y, - const char* str, + std::string s, const int r, const int g, const int b, - const bool cen, - const int linespacing, - const int maxwidth + const bool cen /*= false*/, + int linespacing /*= -1*/, + int maxwidth /*= -1*/ ) { + if (linespacing == -1) + { + linespacing = 10; + } + linespacing = SDL_max(linespacing, loc::get_langmeta()->font_h); + + if (maxwidth == -1) + { + maxwidth = 304; + } + + const char* str = s.c_str(); /* Screen width is 320 pixels. The shortest a char can be is 6 pixels wide. * 320 / 6 is 54, rounded up. 4 bytes per char. */ char buffer[54*4 + 1]; @@ -675,6 +708,8 @@ void Graphics::PrintWrap( y += linespacing; } } + + return y + linespacing; } @@ -723,6 +758,135 @@ int Graphics::len(const std::string& t) return bfontpos; } +std::string Graphics::string_wordwrap(const std::string& s, int maxwidth, short *lines /*= NULL*/) +{ + // Return a string wordwrapped to a maximum limit by adding newlines. + // CJK will need to have autowordwrap disabled and have manually inserted newlines. + + if (lines != NULL) + { + *lines = 1; + } + + const char* orig = s.c_str(); + + std::string result; + size_t start = 0; + bool first = true; + + while (true) + { + size_t len = 0; + const char* part = &orig[start]; + + const bool retval = next_wrap(&start, &len, part, maxwidth); + + if (!retval) + { + return result; + } + + if (first) + { + first = false; + } + else + { + result.push_back('\n'); + + if (lines != NULL) + { + (*lines)++; + } + } + result.append(part, len); + } +} + +std::string Graphics::string_wordwrap_balanced(const std::string& s, int maxwidth) +{ + // Return a string wordwrapped to a limit of maxwidth by adding newlines. + // Try to fill the lines as far as possible, and return result where lines are most filled. + // Goal is to have all lines in textboxes be about as long and to avoid wrapping just one word to a new line. + // CJK will need to have autowordwrap disabled and have manually inserted newlines. + + if (!loc::get_langmeta()->autowordwrap) + { + return s; + } + + short lines; + string_wordwrap(s, maxwidth, &lines); + + int bestwidth = maxwidth; + if (lines > 1) + { + for (int curlimit = maxwidth; curlimit > 1; curlimit -= 8) + { + short try_lines; + string_wordwrap(s, curlimit, &try_lines); + + if (try_lines > lines) + { + bestwidth = curlimit + 8; + break; + } + } + } + + return string_wordwrap(s, bestwidth); +} + +std::string Graphics::string_unwordwrap(const std::string& s) +{ + /* Takes a string wordwrapped by newlines, and turns it into a single line, undoing the wrapping. + * Also trims any leading/trailing whitespace and collapses multiple spaces into one (to undo manual centering) + * Only applied to English, so langmeta.autowordwrap isn't used here (it'd break looking up strings) */ + + std::string result; + std::back_insert_iterator inserter = std::back_inserter(result); + std::string::const_iterator iter = s.begin(); + bool latest_was_space = true; // last character was a space (or the beginning, don't want leading whitespace) + int consecutive_newlines = 0; // number of newlines currently encountered in a row (multiple newlines should stay!) + while (iter != s.end()) + { + uint32_t ch = utf8::unchecked::next(iter); + + if (ch == '\n') + { + if (consecutive_newlines == 0) + { + ch = ' '; + } + else if (consecutive_newlines == 1) + { + // The last character was already a newline, so change it back from the space we thought it should have become. + result[result.size()-1] = '\n'; + } + consecutive_newlines++; + } + else + { + consecutive_newlines = 0; + } + + if (ch != ' ' || !latest_was_space) + { + utf8::unchecked::append(ch, inserter); + } + + latest_was_space = (ch == ' ' || ch == '\n'); + } + + // We could have one trailing space + if (!result.empty() && result[result.size()-1] == ' ') + { + result.erase(result.end()-1); + } + + return result; +} + void Graphics::bprint( int x, int y, const std::string& t, int r, int g, int b, bool cen /*= false*/ ) { bprintalpha(x,y,t,r,g,b,255,cen); } @@ -1520,8 +1684,19 @@ void Graphics::setfade(const int amount) oldfadeamount = amount; } -void Graphics::drawmenu( int cr, int cg, int cb, bool levelmenu /*= false*/ ) +void Graphics::drawmenu(int cr, int cg, int cb, enum Menu::MenuName menu) { + /* The MenuName is only used for some special cases, + * like the levels list and the language screen. */ + + bool language_screen = menu == Menu::language && !loc::languagelist.empty(); + unsigned int twocol_voptions; + if (language_screen) + { + size_t n_options = game.menuoptions.size(); + twocol_voptions = n_options - (n_options/2); + } + for (size_t i = 0; i < game.menuoptions.size(); i++) { MenuOption& opt = game.menuoptions[i]; @@ -1542,11 +1717,21 @@ void Graphics::drawmenu( int cr, int cg, int cb, bool levelmenu /*= false*/ ) fb = 128; } - int x = i*game.menuspacing + game.menuxoff; - int y = 140 + i*12 + game.menuyoff; + int x, y; + if (language_screen) + { + int name_len = len(opt.text); + x = (i < twocol_voptions ? 80 : 240) - name_len/2; + y = 36 + (i % twocol_voptions)*12; + } + else + { + x = i*game.menuspacing + game.menuxoff; + y = 140 + i*12 + game.menuyoff; + } #ifndef NO_CUSTOM_LEVELS - if (levelmenu) + if (menu == Menu::levellist) { size_t separator; if (cl.ListOfMetaData.size() > 8) @@ -1570,31 +1755,28 @@ void Graphics::drawmenu( int cr, int cg, int cb, bool levelmenu /*= false*/ ) } #endif - char tempstring[MENU_TEXT_BYTES]; - SDL_strlcpy(tempstring, opt.text, sizeof(tempstring)); - char buffer[MENU_TEXT_BYTES]; if ((int) i == game.currentmenuoption && game.slidermode == SLIDER_NONE) { + std::string opt_text; if (opt.active) { // Uppercase the text - // FIXME: This isn't UTF-8 aware! - size_t templen = SDL_strlen(tempstring); - for (size_t ii = 0; ii < templen; ii++) - { - tempstring[ii] = SDL_toupper(tempstring[ii]); - } + opt_text = loc::toupper(opt.text); + } + else + { + opt_text = loc::remove_toupper_escape_chars(opt.text); } - // Add brackets - SDL_snprintf(buffer, sizeof(buffer), "[ %s ]", tempstring); + vformat_buf(buffer, sizeof(buffer), loc::get_langmeta()->menu_select.c_str(), "label:str", opt_text.c_str()); + // Account for brackets - x -= 16; + x -= (len(buffer)-len(opt_text))/2; } else { - SDL_strlcpy(buffer, tempstring, sizeof(buffer)); + SDL_strlcpy(buffer, loc::remove_toupper_escape_chars(opt.text).c_str(), sizeof(buffer)); } Print(x, y, buffer, fr, fg, fb); @@ -3112,6 +3294,84 @@ void Graphics::textboxcentery(void) textboxes[m].centery(); } +int Graphics::textboxwrap(int pad) +{ + /* This function just takes a single-line textbox and wraps it... + * pad = the total number of characters we are going to pad this textbox. + * (or how many characters we should stay clear of 288 pixels width in general) + * Only to be used after a manual graphics.createtextbox[flipme] call. + * Returns the new, total height of the textbox. */ + if (!INBOUNDS_VEC(m, textboxes)) + { + vlog_error("textboxwrap() out-of-bounds!"); + return 16; + } + if (textboxes[m].lines.empty()) + { + vlog_error("textboxwrap() has no first line!"); + return 16; + } + std::string wrapped = string_wordwrap_balanced(textboxes[m].lines[0], 36*8 - pad*8); + textboxes[m].lines.clear(); + + size_t startline = 0; + size_t newline; + do { + size_t pos_n = wrapped.find('\n', startline); + size_t pos_p = wrapped.find('|', startline); + newline = SDL_min(pos_n, pos_p); + addline(wrapped.substr(startline, newline-startline)); + startline = newline+1; + } while (newline != std::string::npos); + + return textboxes[m].h; +} + +void Graphics::textboxpad(size_t left_pad, size_t right_pad) +{ + if (!INBOUNDS_VEC(m, textboxes)) + { + vlog_error("textboxpad() out-of-bounds!"); + return; + } + + textboxes[m].pad(left_pad, right_pad); +} + +void Graphics::textboxpadtowidth(size_t new_w) +{ + if (!INBOUNDS_VEC(m, textboxes)) + { + vlog_error("textboxpadtowidth() out-of-bounds!"); + return; + } + + textboxes[m].padtowidth(new_w); +} + +void Graphics::textboxcentertext() +{ + if (!INBOUNDS_VEC(m, textboxes)) + { + vlog_error("textboxcentertext() out-of-bounds!"); + return; + } + + textboxes[m].centertext(); +} + +void Graphics::textboxcommsrelay() +{ + /* Special treatment for the gamestate textboxes in Comms Relay */ + if (!INBOUNDS_VEC(m, textboxes)) + { + vlog_error("textboxcommsrelay() out-of-bounds!"); + return; + } + textboxwrap(11); + textboxes[m].xp = 224 - textboxes[m].w; +} + int Graphics::crewcolour(const int t) { //given crewmate t, return colour in setcol @@ -3417,6 +3677,11 @@ bool Graphics::reloadresources(void) minimap_mounted = FILESYSTEM_isAssetMounted("graphics/minimap.png"); #endif + gamecomplete_mounted = FILESYSTEM_isAssetMounted("graphics/gamecomplete.png"); + levelcomplete_mounted = FILESYSTEM_isAssetMounted("graphics/levelcomplete.png"); + flipgamecomplete_mounted = FILESYSTEM_isAssetMounted("graphics/flipgamecomplete.png"); + fliplevelcomplete_mounted = FILESYSTEM_isAssetMounted("graphics/fliplevelcomplete.png"); + return true; fail: @@ -3442,3 +3707,18 @@ Uint32 Graphics::crewcolourreal(int t) } return col_crewcyan; } + +void Graphics::render_roomname(const char* roomname, int r, int g, int b) +{ + footerrect.y = 230; + if (translucentroomname) + { + SDL_BlitSurface(footerbuffer, NULL, backBuffer, &footerrect); + bprint(5, 231, roomname, r, g, b, true); + } + else + { + FillRect(backBuffer, footerrect, 0); + Print(5, 231, roomname, r, g, b, true); + } +} diff --git a/desktop_version/src/Graphics.h b/desktop_version/src/Graphics.h index 51c50fb9..c6ce8771 100644 --- a/desktop_version/src/Graphics.h +++ b/desktop_version/src/Graphics.h @@ -5,6 +5,7 @@ #include #include +#include "Game.h" #include "GraphicsResources.h" #include "GraphicsUtil.h" #include "Maths.h" @@ -53,7 +54,7 @@ public: void drawcoloredtile(int x, int y, int t, int r, int g, int b); - void drawmenu(int cr, int cg, int cb, bool levelmenu = false); + void drawmenu(int cr, int cg, int cb, enum Menu::MenuName menu); void processfade(void); void setfade(const int amount); @@ -96,6 +97,16 @@ public: void textboxcentery(void); + int textboxwrap(int pad); + + void textboxpad(size_t left_pad, size_t right_pad); + + void textboxpadtowidth(size_t new_w); + + void textboxcentertext(); + + void textboxcommsrelay(); + void textboxadjust(void); void addline(const std::string& t); @@ -152,13 +163,17 @@ public: bool next_wrap_s(char buffer[], size_t buffer_size, size_t* start, const char* str, int maxwidth); - void PrintWrap(int x, int y, const char* str, int r, int g, int b, bool cen, int linespacing, int maxwidth); + int PrintWrap(int x, int y, std::string s, int r, int g, int b, bool cen = false, int linespacing = -1, int maxwidth = -1); void bprint(int x, int y, const std::string& t, int r, int g, int b, bool cen = false); void bprintalpha(int x, int y, const std::string& t, int r, int g, int b, int a, bool cen = false); int len(const std::string& t); + std::string string_wordwrap(const std::string& s, int maxwidth, short *lines = NULL); + std::string string_wordwrap_balanced(const std::string& s, int maxwidth); + std::string string_unwordwrap(const std::string& s); + void bigprint( int _x, int _y, const std::string& _s, int r, int g, int b, bool cen = false, int sc = 2 ); void bigbprint(int x, int y, const std::string& s, int r, int g, int b, bool cen = false, int sc = 2); void drawspritesetcol(int x, int y, int t, int c); @@ -235,6 +250,11 @@ public: bool minimap_mounted; #endif + bool gamecomplete_mounted; + bool levelcomplete_mounted; + bool flipgamecomplete_mounted; + bool fliplevelcomplete_mounted; + void menuoffrender(void); @@ -335,10 +355,16 @@ public: SDL_Surface* ghostbuffer; +#ifndef GAME_DEFINITION float inline lerp(const float v0, const float v1) { + if (game.physics_frozen()) + { + return v1; + } return v0 + alpha * (v1 - v0); } +#endif float alpha; Uint32 col_crewred; @@ -359,6 +385,8 @@ public: Uint32 crewcolourreal(int t); + void render_roomname(const char* roomname, int r, int g, int b); + char error[128]; char error_title[128]; /* for SDL_ShowSimpleMessageBox */ }; diff --git a/desktop_version/src/Input.cpp b/desktop_version/src/Input.cpp index fc124032..f2641486 100644 --- a/desktop_version/src/Input.cpp +++ b/desktop_version/src/Input.cpp @@ -10,9 +10,13 @@ #include "GlitchrunnerMode.h" #include "Graphics.h" #include "KeyPoll.h" +#include "Localization.h" +#include "LocalizationMaint.h" +#include "LocalizationStorage.h" #include "MakeAndPlay.h" #include "Map.h" #include "Music.h" +#include "RoomnameTranslator.h" #include "Screen.h" #include "Script.h" #include "UtilityClass.h" @@ -361,24 +365,47 @@ static void slidermodeinput(void) static void menuactionpress(void) { + if (game.menutestmode) + { + music.playef(6); + Menu::MenuName nextmenu = (Menu::MenuName) (game.currentmenuname + 1); + game.returnmenu(); + game.createmenu(nextmenu); + return; + } + switch (game.currentmenuname) { case Menu::mainmenu: -#if defined(MAKEANDPLAY) -#define MPOFFSET -1 -#else -#define MPOFFSET 0 + { + int option_id = -1; + int option_seq = 0; /* option number in YOUR configuration */ +#define OPTION_ID(id) \ + if (option_seq == game.currentmenuoption) \ + { \ + option_id = id; \ + } \ + option_seq++; +#if !defined(MAKEANDPLAY) + OPTION_ID(0) /* play */ #endif - -#if defined(NO_CUSTOM_LEVELS) -#define NOCUSTOMSOFFSET -1 -#else -#define NOCUSTOMSOFFSET 0 +#if !defined(NO_CUSTOM_LEVELS) + OPTION_ID(1) /* levels */ #endif + OPTION_ID(2) /* options */ + if (loc::show_translator_menu) + { + OPTION_ID(3) /* translator */ + } +#if !defined(MAKEANDPLAY) + OPTION_ID(4) /* credits */ +#endif + OPTION_ID(5) /* quit */ -#define OFFSET (MPOFFSET+NOCUSTOMSOFFSET) +#undef OPTION_ID - switch (game.currentmenuoption) + + switch (option_id) { #if !defined(MAKEANDPLAY) case 0: @@ -399,40 +426,41 @@ static void menuactionpress(void) break; #endif #if !defined(NO_CUSTOM_LEVELS) - case OFFSET+1: + case 1: //Bring you to the normal playmenu music.playef(11); game.createmenu(Menu::playerworlds); map.nexttowercolour(); break; #endif - case OFFSET+2: + case 2: //Options music.playef(11); game.createmenu(Menu::options); map.nexttowercolour(); break; + case 3: + //Translator + music.playef(11); + game.createmenu(Menu::translator_main); + map.nexttowercolour(); + break; #if !defined(MAKEANDPLAY) - case OFFSET+3: + case 4: //Credits music.playef(11); game.createmenu(Menu::credits); map.nexttowercolour(); break; -#else - #undef MPOFFSET - #define MPOFFSET -2 #endif - case OFFSET+4: + case 5: music.playef(11); game.createmenu(Menu::youwannaquit); map.nexttowercolour(); break; -#undef OFFSET -#undef NOCUSTOMSOFFSET -#undef MPOFFSET } break; + } #if !defined(NO_CUSTOM_LEVELS) case Menu::levellist: { @@ -1010,6 +1038,14 @@ static void menuactionpress(void) game.createmenu(Menu::accessibility); map.nexttowercolour(); break; + case 5: + //language options + music.playef(11); + loc::loadlanguagelist(); + game.createmenu(Menu::language); + game.currentmenuoption = loc::languagelist_curlang; + map.nexttowercolour(); + break; default: /* Return */ music.playef(11); @@ -1065,6 +1101,181 @@ static void menuactionpress(void) music.playef(11); } break; + case Menu::language: + { + music.playef(11); + + bool show_title = !loc::lang_set; + + if (loc::languagelist.size() != 0 && (unsigned)game.currentmenuoption < loc::languagelist.size()) + { + loc::lang = loc::languagelist[game.currentmenuoption].code; + loc::loadtext(false); + loc::lang_set = true; + } + + if (show_title) + { + /* Make the title screen appear, we haven't seen it yet */ + game.menustart = false; + game.createmenu(Menu::mainmenu); + game.currentmenuoption = 0; + } + else + { + game.returnmenu(); + } + map.nexttowercolour(); + game.savestatsandsettings_menu(); + + break; + } + case Menu::translator_main: + switch (game.currentmenuoption) + { + case 0: + // translator options + music.playef(11); + game.createmenu(Menu::translator_options); + map.nexttowercolour(); + break; + case 1: + // maintenance + music.playef(11); + game.createmenu(Menu::translator_maintenance); + map.nexttowercolour(); + break; + case 2: + // open lang folder + if (FILESYSTEM_openDirectoryEnabled() + && FILESYSTEM_openDirectory(FILESYSTEM_getUserMainLangDirectory())) + { + music.playef(11); + SDL_MinimizeWindow(gameScreen.m_window); + } + else + { + music.playef(2); + } + break; + default: + // return + music.playef(11); + game.returnmenu(); + map.nexttowercolour(); + break; + } + break; + case Menu::translator_options: + switch (game.currentmenuoption) + { + case 0: + // language statistics + music.playef(11); + game.createmenu(Menu::translator_options_stats); + map.nexttowercolour(); + break; + case 1: + // translate room names + music.playef(11); + roomname_translator::set_enabled(!roomname_translator::enabled); + game.savestatsandsettings_menu(); + break; + case 2: + // menu test + music.playef(18); + game.menutestmode = true; + game.createmenu((Menu::MenuName) 0); + map.nexttowercolour(); + break; + case 3: + // limits check + music.playef(11); + loc::local_limits_check(); + game.createmenu(Menu::translator_options_limitscheck); + map.nexttowercolour(); + break; + default: + // return + music.playef(11); + game.returnmenu(); + map.nexttowercolour(); + break; + } + break; + case Menu::translator_options_limitscheck: + switch (game.currentmenuoption) + { + case 0: + // next + if (loc::limitscheck_current_overflow < loc::text_overflows.size()) + { + music.playef(11); + loc::limitscheck_current_overflow++; + } + break; + default: + // return + music.playef(11); + game.returnmenu(); + map.nexttowercolour(); + break; + } + break; + case Menu::translator_options_stats: + music.playef(11); + game.returnmenu(); + map.nexttowercolour(); + break; + case Menu::translator_maintenance: + music.playef(11); + switch (game.currentmenuoption) + { + case 0: + // sync languages + game.createmenu(Menu::translator_maintenance_sync); + map.nexttowercolour(); + break; + case 1: + // global statistics + // TODO + map.nexttowercolour(); + break; + case 2: + // global limits check + loc::global_limits_check(); + game.createmenu(Menu::translator_options_limitscheck); + map.nexttowercolour(); + break; + default: + // return + game.returnmenu(); + map.nexttowercolour(); + break; + } + break; + case Menu::translator_maintenance_sync: + { + music.playef(11); + bool sync_success = true; + if (game.currentmenuoption == 0) + { + // yes, sync files + sync_success = loc::sync_lang_files(); + } + game.returnmenu(); + map.nexttowercolour(); + if (!sync_success) + { + game.createmenu(Menu::translator_error_setlangwritedir); + } + break; + } + case Menu::translator_error_setlangwritedir: + music.playef(11); + game.returnmenu(); + map.nexttowercolour(); + break; case Menu::unlockmenutrials: switch (game.currentmenuoption) { @@ -1561,7 +1772,7 @@ static void menuactionpress(void) map.nexttowercolour(); break; case Menu::playmodes: - if (game.currentmenuoption == 0 && !game.nocompetitive()) //go to the time trial menu + if (game.currentmenuoption == 0 && !game.nocompetitive_unless_translator()) //go to the time trial menu { music.playef(11); game.createmenu(Menu::timetrials); @@ -1828,11 +2039,30 @@ void titleinput(void) game.press_map = false; game.press_interact = false; + bool lang_press_horizontal = false; + if (graphics.flipmode) { if (key.isDown(KEYBOARD_LEFT) || key.isDown(KEYBOARD_DOWN) || key.isDown(KEYBOARD_a) || key.isDown(KEYBOARD_s) || key.controllerWantsRight(true)) game.press_left = true; if (key.isDown(KEYBOARD_RIGHT) || key.isDown(KEYBOARD_UP) || key.isDown(KEYBOARD_d) || key.isDown(KEYBOARD_w) || key.controllerWantsLeft(true)) game.press_right = true; } + else if (game.currentmenuname == Menu::language) + { + if (key.isDown(KEYBOARD_UP) || key.isDown(KEYBOARD_w) || key.controllerWantsUp()) + { + game.press_left = true; + } + if (key.isDown(KEYBOARD_DOWN) || key.isDown(KEYBOARD_s) || key.controllerWantsDown()) + { + game.press_right = true; + } + if (key.isDown(KEYBOARD_LEFT) || key.isDown(KEYBOARD_a) || key.controllerWantsLeft(false) + || key.isDown(KEYBOARD_RIGHT) || key.isDown(KEYBOARD_d) || key.controllerWantsRight(false)) + { + lang_press_horizontal = true; + game.press_right = true; + } + } else { if (key.isDown(KEYBOARD_LEFT) || key.isDown(KEYBOARD_UP) || key.isDown(KEYBOARD_a) || key.isDown(KEYBOARD_w) || key.controllerWantsLeft(true)) @@ -1863,8 +2093,23 @@ void titleinput(void) && game.menucountdown <= 0 && (key.isDown(27) || key.isDown(game.controllerButton_esc))) { - music.playef(11); - if (game.currentmenuname == Menu::mainmenu) + if (game.currentmenuname == Menu::language && !loc::lang_set) + { + /* Don't exit from the initial language screen, + * you can't do this on the loading/title screen either. */ + return; + } + else + { + music.playef(11); + } + if (game.menutestmode) + { + game.menutestmode = false; + game.returnmenu(); + map.nexttowercolour(); + } + else if (game.currentmenuname == Menu::mainmenu) { game.createmenu(Menu::youwannaquit); map.nexttowercolour(); @@ -1908,7 +2153,63 @@ void titleinput(void) { if (game.slidermode == SLIDER_NONE) { - if (game.press_left) + if (game.currentmenuname == Menu::language) + { + /* The language screen has two columns and navigation in four directions. + * The second column may have one less option than the first. */ + int n_options = game.menuoptions.size(); + int twocol_voptions = n_options - (n_options/2); + + if (lang_press_horizontal) + { + if (game.currentmenuoption < twocol_voptions) + { + game.currentmenuoption += twocol_voptions; + if (game.currentmenuoption >= n_options) + { + game.currentmenuoption = n_options - 1; + } + } + else + { + game.currentmenuoption -= twocol_voptions; + } + } + else + { + /* Vertical movement */ + int min_option; + int max_option; + if (game.currentmenuoption < twocol_voptions) + { + min_option = 0; + max_option = twocol_voptions-1; + } + else + { + min_option = twocol_voptions; + max_option = n_options-1; + } + + if (game.press_left) /* Up, lol */ + { + game.currentmenuoption--; + if (game.currentmenuoption < min_option) + { + game.currentmenuoption = max_option; + } + } + else if (game.press_right) /* Down, lol */ + { + game.currentmenuoption++; + if (game.currentmenuoption > max_option) + { + game.currentmenuoption = min_option; + } + } + } + } + else if (game.press_left) { game.currentmenuoption--; } @@ -1967,6 +2268,11 @@ void gameinput(void) if(!script.running) { + if (roomname_translator::enabled && roomname_translator::overlay_input()) + { + return; + } + game.press_left = false; game.press_right = false; game.press_action = false; diff --git a/desktop_version/src/KeyPoll.cpp b/desktop_version/src/KeyPoll.cpp index 91bc82cd..9dfdcb21 100644 --- a/desktop_version/src/KeyPoll.cpp +++ b/desktop_version/src/KeyPoll.cpp @@ -9,6 +9,8 @@ #include "Game.h" #include "GlitchrunnerMode.h" #include "Graphics.h" +#include "Localization.h" +#include "LocalizationStorage.h" #include "Music.h" #include "Screen.h" #include "Vlogging.h" @@ -165,6 +167,13 @@ void KeyPoll::Poll(void) fullscreenkeybind = true; } + if (loc::show_translator_menu && evt.key.keysym.sym == SDLK_F12 && !evt.key.repeat) + { + /* Reload language files */ + loc::loadtext(false); + music.playef(4); + } + if (textentry()) { if (evt.key.keysym.sym == SDLK_BACKSPACE && !keybuffer.empty()) @@ -470,3 +479,13 @@ bool KeyPoll::controllerWantsRight(bool includeVert) ( buttonmap[SDL_CONTROLLER_BUTTON_DPAD_DOWN] || yVel > 0 ) ) ); } + +bool KeyPoll::controllerWantsUp(void) +{ + return buttonmap[SDL_CONTROLLER_BUTTON_DPAD_UP] || yVel < 0; +} + +bool KeyPoll::controllerWantsDown(void) +{ + return buttonmap[SDL_CONTROLLER_BUTTON_DPAD_DOWN] || yVel > 0; +} diff --git a/desktop_version/src/KeyPoll.h b/desktop_version/src/KeyPoll.h index 91069c93..d6c40b99 100644 --- a/desktop_version/src/KeyPoll.h +++ b/desktop_version/src/KeyPoll.h @@ -59,6 +59,8 @@ public: bool controllerButtonDown(void); bool controllerWantsLeft(bool includeVert); bool controllerWantsRight(bool includeVert); + bool controllerWantsUp(void); + bool controllerWantsDown(void); int leftbutton, rightbutton, middlebutton; int mx, my; diff --git a/desktop_version/src/Logic.cpp b/desktop_version/src/Logic.cpp index 16cdede8..8fd9e60c 100644 --- a/desktop_version/src/Logic.cpp +++ b/desktop_version/src/Logic.cpp @@ -127,6 +127,11 @@ static void gotoroom_wrapper(const int rx, const int ry) void gamelogic(void) { + if (game.physics_frozen()) + { + return; + } + bool roomchange = false; #define GOTOROOM(rx, ry) \ gotoroom_wrapper(rx, ry); \ diff --git a/desktop_version/src/Render.cpp b/desktop_version/src/Render.cpp index bda5fdbb..eeb4c460 100644 --- a/desktop_version/src/Render.cpp +++ b/desktop_version/src/Render.cpp @@ -11,14 +11,18 @@ #include "GraphicsUtil.h" #include "InterimVersion.h" #include "KeyPoll.h" +#include "Localization.h" +#include "LocalizationStorage.h" #include "MakeAndPlay.h" #include "Map.h" #include "Maths.h" #include "Music.h" #include "ReleaseVersion.h" +#include "RoomnameTranslator.h" #include "Screen.h" #include "Script.h" #include "UtilityClass.h" +#include "VFormat.h" static int tr; static int tg; @@ -294,6 +298,9 @@ static void menurender(void) graphics.Print(-1, 65, "Disable screen effects, enable", tr, tg, tb, true); graphics.Print(-1, 75, "slowdown modes or invincibility.", tr, tg, tb, true); break; + case 5: + graphics.bigprint( -1, 30, loc::gettext("Language"), tr, tg, tb, true); + graphics.PrintWrap( -1, 65, loc::gettext("Change the language."), tr, tg, tb, true); } break; case Menu::graphicoptions: @@ -606,6 +613,186 @@ static void menurender(void) } + break; + case Menu::language: + if (loc::languagelist.empty()) + { + graphics.PrintWrap(-1, 90, loc::gettext("ERROR: No language files found."), tr, tg, tb, true); + } + else if ((unsigned)game.currentmenuoption < loc::languagelist.size()) + { + graphics.PrintWrap(-1, 8, loc::languagelist[game.currentmenuoption].credit, tr/2, tg/2, tb/2, true); + graphics.Print(-1, 230, loc::languagelist[game.currentmenuoption].action_hint, tr/2, tg/2, tb/2, true); + } + break; + case Menu::translator_main: + switch (game.currentmenuoption) + { + case 0: + graphics.bigprint( -1, 30, loc::gettext("Translator options"), tr, tg, tb, true); + graphics.PrintWrap( -1, 65, loc::gettext("Some options that are useful for translators and developers."), tr, tg, tb, true); + break; + case 1: + graphics.bigprint( -1, 30, loc::gettext("Maintenance"), tr, tg, tb, true); + graphics.PrintWrap( -1, 65, loc::gettext("Sync all language files after adding new strings."), tr, tg, tb, true); + break; + } + { + if (FILESYSTEM_isMainLangDirFromRepo()) + { + // Just giving people who manually compiled the game some hint as to why this menu is here! + graphics.Print(8, 208, loc::gettext("Repository language folder:"), tr/2, tg/2, tb/2); + } + else + { + graphics.Print(8, 208, loc::gettext("Language folder:"), tr/2, tg/2, tb/2); + } + + char* mainLangDir = FILESYSTEM_getUserMainLangDirectory(); + graphics.Print(316-graphics.len(mainLangDir), 224, mainLangDir, tr/2, tg/2, tb/2); + } + break; + case Menu::translator_options: + switch (game.currentmenuoption) + { + case 0: + graphics.bigprint( -1, 30, loc::gettext("Statistics"), tr, tg, tb, true); + graphics.PrintWrap( -1, 65, loc::gettext("Count the amount of untranslated strings for this language."), tr, tg, tb, true); + break; + case 1: + { + graphics.bigprint( -1, 30, loc::gettext("Translate rooms"), tr, tg, tb, true); + int next_y = graphics.PrintWrap( -1, 65, loc::gettext("Enable room name translation mode, so you can translate room names in context."), tr, tg, tb, true); + + if (roomname_translator::enabled) + { + graphics.PrintWrap( -1, next_y, loc::gettext("Currently ENABLED!"), tr, tg, tb, true); + } + else + { + graphics.PrintWrap( -1, next_y, loc::gettext("Currently Disabled."), tr/2, tg/2, tb/2, true); + } + break; + } + case 2: + graphics.bigprint( -1, 30, loc::gettext("Menu test"), tr, tg, tb, true); + graphics.PrintWrap( -1, 65, loc::gettext("Cycle through most menus in the game. The menus will not actually work, all options take you to the next menu instead. Press Escape to stop."), tr, tg, tb, true); + break; + case 3: + graphics.bigprint( -1, 30, loc::gettext("Limits check"), tr, tg, tb, true); + graphics.PrintWrap( -1, 65, loc::gettext("Find translations that don't fit within their defined bounds."), tr, tg, tb, true); + break; + } + break; + case Menu::translator_options_limitscheck: + { + size_t of = loc::limitscheck_current_overflow; + if (of >= loc::text_overflows.size()) + { + int next_y; + if (loc::text_overflows.empty()) + { + next_y = graphics.PrintWrap(-1, 20, loc::gettext("No text overflows found!"), tr, tg, tb, true); + } + else + { + next_y = graphics.PrintWrap(-1, 20, loc::gettext("No text overflows left!"), tr, tg, tb, true); + } + + graphics.PrintWrap(-1, next_y, loc::gettext("Note that this detection isn't perfect."), tr, tg, tb, true); + } + else + { + loc::TextOverflow& overflow = loc::text_overflows[of]; + + char buffer[SCREEN_WIDTH_CHARS + 1]; + vformat_buf(buffer, sizeof(buffer), + "{page}/{total} {max_w}*{max_h} ({max_w_px}x{max_h_px}) [{lang}]", + "page:int, total:int, max_w:int, max_h:int, max_w_px:int, max_h_px:int, lang:str", + (int) of+1, (int) loc::text_overflows.size(), + overflow.max_w, overflow.max_h, + overflow.max_w_px, overflow.max_h_px, + overflow.lang.c_str() + ); + graphics.Print(10, 10, buffer, tr/2, tg/2, tb/2); + + int box_x = SDL_min(10, (320-overflow.max_w_px)/2); + int box_h = overflow.max_h_px - SDL_max(0, 10-loc::get_langmeta()->font_h); + FillRect(graphics.backBuffer, box_x-1, 30-1, overflow.max_w_px+2, box_h+2, tr/3, tg/3, tb/3); + + int wraplimit; + if (overflow.multiline) + { + wraplimit = overflow.max_w_px; + } + else + { + wraplimit = 320-box_x; + } + + if (overflow.text != NULL) + { + graphics.PrintWrap(box_x, 30, overflow.text, tr, tg, tb, false, -1, wraplimit); + } + } + break; + } + case Menu::translator_options_stats: + { + graphics.Print(16, 16, loc::get_langmeta()->nativename, tr, tg, tb); + + const char* line_template = "%4d"; + char buffer[5]; + int coldiv; + + #define stat_line(y, filename, untranslated_counter) \ + SDL_snprintf(buffer, sizeof(buffer), line_template, \ + untranslated_counter \ + ); \ + coldiv = untranslated_counter > 0 ? 1 : 2; \ + graphics.Print(16, y, filename, tr/coldiv, tg/coldiv, tb/coldiv); \ + graphics.Print(272, y, buffer, tr/coldiv, tg/coldiv, tb/coldiv) + + stat_line(48, "strings.xml", loc::n_untranslated[loc::UNTRANSLATED_STRINGS]); + stat_line(64, "numbers.xml", loc::n_untranslated[loc::UNTRANSLATED_NUMBERS]); + stat_line(80, "strings_plural.xml", loc::n_untranslated[loc::UNTRANSLATED_STRINGS_PLURAL]); + stat_line(96, "cutscenes.xml", loc::n_untranslated[loc::UNTRANSLATED_CUTSCENES]); + stat_line(112, "roomnames.xml", loc::n_untranslated_roomnames); + stat_line(128, "roomnames_special.xml", loc::n_untranslated[loc::UNTRANSLATED_ROOMNAMES_SPECIAL]); + + #undef stat_line + + break; + } + case Menu::translator_maintenance: + switch (game.currentmenuoption) + { + case 0: + graphics.bigprint( -1, 30, loc::gettext("Sync language files"), tr, tg, tb, true); + graphics.PrintWrap( -1, 65, loc::gettext("Merge all new strings from the template files into the translation files, keeping existing translations."), tr, tg, tb, true); + break; + case 1: + graphics.bigprint( -1, 30, loc::gettext("Statistics"), tr, tg, tb, true); + graphics.PrintWrap( -1, 65, loc::gettext("Count the amount of untranslated strings for each language."), tr, tg, tb, true); + break; + case 2: + graphics.bigprint( -1, 30, loc::gettext("Limits check"), tr, tg, tb, true); + graphics.PrintWrap( -1, 65, loc::gettext("Find translations that don't fit within their defined bounds."), tr, tg, tb, true); + } + break; + case Menu::translator_maintenance_sync: + { + int next_y = graphics.PrintWrap(-1, 20, loc::gettext("If new strings were added to the English template language files, this feature will insert them in the translation files for all languages. Make a backup, just in case."), tr, tg, tb, true); + + graphics.Print(-1, next_y, loc::gettext("Full syncing EN→All:"), tr, tg, tb, true); + next_y = graphics.PrintWrap(-1, next_y+10, "meta.xml\nstrings.xml\nstrings_plural.xml\ncutscenes.xml\nroomnames.xml\nroomnames_special.xml", tr/2, tg/2, tb/2, true); + + graphics.Print(-1, next_y, loc::gettext("Syncing not supported:"), tr, tg, tb, true); + graphics.PrintWrap(-1, next_y+10, "numbers.xml", tr/2, tg/2, tb/2, true); + break; + } + case Menu::translator_error_setlangwritedir: + graphics.PrintWrap( -1, 95, loc::gettext("ERROR: Could not write to language folder! Make sure there is no \"lang\" folder next to the regular saves."), tr, tg, tb, true); break; case Menu::speedrunneroptions: switch (game.currentmenuoption) @@ -1460,7 +1647,7 @@ void titlerender(void) if(tg>255) tg=255; if (tb < 0) tb = 0; if(tb>255) tb=255; - graphics.drawmenu(tr, tg, tb, game.currentmenuname == Menu::levellist); + graphics.drawmenu(tr, tg, tb, game.currentmenuname); } graphics.drawfade(); @@ -1728,7 +1915,8 @@ void gamerender(void) && !game.intimetrial && !game.isingamecompletescreen() && (!game.swnmode || game.swngame != 1) - && game.showingametimer) + && game.showingametimer + && !roomname_translator::enabled) { char buffer[SCREEN_WIDTH_CHARS + 1]; graphics.bprint(6, 6, "TIME:", 255,255,255); @@ -1736,31 +1924,30 @@ void gamerender(void) graphics.bprint(46, 6, buffer, 196, 196, 196); } - if(map.extrarow==0 || (map.custommode && map.roomname[0] != '\0')) + bool force_roomname_hidden = false; + int roomname_r = 196, roomname_g = 196, roomname_b = 255 - help.glow; + if (roomname_translator::enabled) + { + roomname_translator::overlay_render( + &force_roomname_hidden, + &roomname_r, &roomname_g, &roomname_b + ); + } + + if ((map.extrarow==0 || (map.custommode && map.roomname[0] != '\0')) && !force_roomname_hidden) { const char* roomname; - graphics.footerrect.y = 230; - if (map.finalmode) { - roomname = map.glitchname; + roomname = loc::gettext_roomname(map.custommode, game.roomx, game.roomy, map.glitchname, map.roomname_special); } else { - roomname = map.roomname; + roomname = loc::gettext_roomname(map.custommode, game.roomx, game.roomy, map.roomname, map.roomname_special); } - if (graphics.translucentroomname) - { - SDL_BlitSurface(graphics.footerbuffer, NULL, graphics.backBuffer, &graphics.footerrect); - graphics.bprint(5, 231, roomname, 196, 196, 255 - help.glow, true); - } - else - { - FillRect(graphics.backBuffer, graphics.footerrect, 0); - graphics.Print(5, 231, roomname, 196, 196, 255 - help.glow, true); - } + graphics.render_roomname(roomname, roomname_r, roomname_g, roomname_b); } if (map.roomtexton) @@ -1945,7 +2132,7 @@ void gamerender(void) graphics.bigbprint( -1, 100, "3", 220 - (help.glow), 220 - (help.glow), 255 - (help.glow / 2), true, 4); } } - else + else if (!roomname_translator::is_pausing()) { char buffer[SCREEN_WIDTH_CHARS + 1]; game.timestringcenti(buffer, sizeof(buffer)); @@ -2031,15 +2218,15 @@ static void draw_roomname_menu(void) if (map.hiddenname[0] != '\0') { - name = map.hiddenname; + name = loc::gettext_roomname_special(map.hiddenname); } else if (map.finalmode) { - name = map.glitchname; + name = loc::gettext_roomname(map.custommode, game.roomx, game.roomy, map.glitchname, map.roomname_special); } else { - name = map.roomname; + name = loc::gettext_roomname(map.custommode, game.roomx, game.roomy, map.roomname, map.roomname_special); } graphics.Print(5, 2, name, 196, 196, 255 - help.glow, true); diff --git a/desktop_version/src/Script.cpp b/desktop_version/src/Script.cpp index f1bd5dfc..f9277050 100644 --- a/desktop_version/src/Script.cpp +++ b/desktop_version/src/Script.cpp @@ -4,6 +4,7 @@ #include #include +#include "Constants.h" #include "CustomLevels.h" #include "Editor.h" #include "Entity.h" @@ -12,10 +13,13 @@ #include "GlitchrunnerMode.h" #include "Graphics.h" #include "KeyPoll.h" +#include "Localization.h" +#include "LocalizationStorage.h" #include "Map.h" #include "Music.h" #include "Unreachable.h" #include "UtilityClass.h" +#include "VFormat.h" #include "Vlogging.h" #include "Xoshiro.h" @@ -36,6 +40,11 @@ scriptclass::scriptclass(void) textx = 0; texty = 0; textflipme = false; + textcentertext = false; + textpad_left = 0; + textpad_right = 0; + textpadtowidth = 0; + textcase = 1; } void scriptclass::clearcustom(void) @@ -504,6 +513,13 @@ void scriptclass::run(void) txt.push_back(commands[position]); } } + + textcentertext = false; + textpad_left = 0; + textpad_right = 0; + textpadtowidth = 0; + + translate_dialogue(); } else if (words[0] == "position") { @@ -690,6 +706,20 @@ void scriptclass::run(void) } } + // Some textbox formatting that can be set by translations... + if (textcentertext) + { + graphics.textboxcentertext(); + } + if (textpad_left > 0 || textpad_right > 0) + { + graphics.textboxpad(textpad_left, textpad_right); + } + if (textpadtowidth > 0) + { + graphics.textboxpadtowidth(textpadtowidth); + } + //the textbox cannot be outside the screen. Fix if it is. if (textx <= -1000) { @@ -1840,6 +1870,7 @@ void scriptclass::run(void) } else if (words[0] == "specialline") { + //Localization is handled with regular cutscene dialogue switch(ss_toi(words[1])) { case 1: @@ -1862,6 +1893,8 @@ void scriptclass::run(void) } break; } + + translate_dialogue(); } else if (words[0] == "trinketbluecontrol") { @@ -2339,6 +2372,27 @@ void scriptclass::run(void) } } } + else if (words[0] == "textcase") + { + // Used to disambiguate identical textboxes for translations (1 by default) + textcase = ss_toi(words[1]); + } + else if (words[0] == "loadtext") + { + if (map.custommode) + { + loc::lang_custom = words[1]; + loc::loadtext_custom(NULL); + } + } + else if (words[0] == "iflang") + { + if (loc::lang == words[1]) + { + load("custom_" + raw_words[2]); + position--; + } + } position++; } @@ -2365,6 +2419,68 @@ void scriptclass::run(void) } } +void scriptclass::translate_dialogue(void) +{ + char tc = textcase; + textcase = 1; + + if (!loc::is_cutscene_translated(scriptname)) + { + return; + } + + // English text needs to be un-wordwrapped, translated, and re-wordwrapped + std::string eng; + for (size_t i = 0; i < txt.size(); i++) + { + if (i != 0) + { + eng.append("\n"); + } + eng.append(txt[i]); + } + + eng = graphics.string_unwordwrap(eng); + const loc::TextboxFormat* format = loc::gettext_cutscene(scriptname, eng, tc); + if (format == NULL || format->text == NULL || format->text[0] == '\0') + { + return; + } + std::string tra; + if (format->tt) + { + tra = std::string(format->text); + size_t pipe; + while (true) + { + pipe = tra.find('|', 0); + if (pipe == std::string::npos) + { + break; + } + tra.replace(pipe, 1, "\n"); + } + } + else + { + tra = graphics.string_wordwrap_balanced(format->text, format->wraplimit); + } + + textcentertext = format->centertext; + textpad_left = format->pad_left; + textpad_right = format->pad_right; + textpadtowidth = format->padtowidth; + + txt.clear(); + size_t startline = 0; + size_t newline; + do { + newline = tra.find('\n', startline); + txt.push_back(tra.substr(startline, newline-startline)); + startline = newline+1; + } while (newline != std::string::npos); +} + static void gotoerrorloadinglevel(void) { game.createmenu(Menu::errorloadinglevel); @@ -2474,9 +2590,13 @@ void scriptclass::startgamemode(const enum StartMode mode) game.nocutscenes = true; game.intimetrial = true; game.timetrialcountdown = 150; - game.timetrialparlost = false; game.timetriallevel = mode - Start_FIRST_TIMETRIAL; + if (map.invincibility) + { + game.sabotage_time_trial(); + } + switch (mode) { case Start_TIMETRIAL_SPACESTATION1: @@ -2912,6 +3032,7 @@ void scriptclass::hardreset(void) game.timetrialshinytarget = 0; game.timetrialparlost = false; game.timetrialpar = 0; + game.timetrialcheater = false; game.totalflips = 0; game.hardestroom = "Welcome Aboard"; @@ -3241,6 +3362,15 @@ void scriptclass::loadcustom(const std::string& t) }else if(words[0] == "iftrinketsless"){ if(customtextmode==1){ add("endtext"); customtextmode=0;} add("custom"+lines[i]); + }else if(words[0] == "textcase"){ + if(customtextmode==1){ add("endtext"); customtextmode=0;} + add(lines[i]); + }else if(words[0] == "iflang"){ + if(customtextmode==1){ add("endtext"); customtextmode=0;} + add(lines[i]); + }else if(words[0] == "loadtext"){ + if(customtextmode==1){ add("endtext"); customtextmode=0;} + add(lines[i]); }else if(words[0] == "destroy"){ if(customtextmode==1){ add("endtext"); customtextmode=0;} add(lines[i]); diff --git a/desktop_version/src/Script.h b/desktop_version/src/Script.h index 248adad4..4c29f2e6 100644 --- a/desktop_version/src/Script.h +++ b/desktop_version/src/Script.h @@ -77,6 +77,8 @@ public: void run(void); + void translate_dialogue(void); + void startgamemode(enum StartMode mode); void teleport(void); @@ -99,6 +101,11 @@ public: int texty; int r,g,b; bool textflipme; + bool textcentertext; + size_t textpad_left; + size_t textpad_right; + size_t textpadtowidth; + char textcase; //Misc int i, j, k; diff --git a/desktop_version/src/Scripts.cpp b/desktop_version/src/Scripts.cpp index bcb89243..9a3bacfc 100644 --- a/desktop_version/src/Scripts.cpp +++ b/desktop_version/src/Scripts.cpp @@ -7,6 +7,7 @@ void scriptclass::load(const std::string& name) //loads script name t into the array position = 0; commands.clear(); + scriptname = name; running = true; const char* t = name.c_str(); diff --git a/desktop_version/src/Textbox.cpp b/desktop_version/src/Textbox.cpp index 166449e0..b4577e7d 100644 --- a/desktop_version/src/Textbox.cpp +++ b/desktop_version/src/Textbox.cpp @@ -1,5 +1,6 @@ #include "Textbox.h" +#include #include textboxclass::textboxclass(void) @@ -103,8 +104,9 @@ void textboxclass::resize(void) if (len > (unsigned int)max) max = len; } - w = (max +2) * 8; - h = (lines.size() + 2) * 8; + // 16 for the borders + w = max*8 + 16; + h = lines.size()*8 + 16; } void textboxclass::addline(const std::string& t) @@ -113,3 +115,40 @@ void textboxclass::addline(const std::string& t) resize(); if ((int) lines.size() >= 12) lines.clear(); } + +void textboxclass::pad(size_t left_pad, size_t right_pad) +{ + // Pad the current text with a certain number of spaces on the left and right + for (size_t iter = 0; iter < lines.size(); iter++) + { + lines[iter] = std::string(left_pad, ' ') + lines[iter] + std::string(right_pad, ' '); + } + resize(); +} + +void textboxclass::padtowidth(size_t new_w) +{ + /* Pad the current text so that each line is new_w pixels wide. + * Each existing line is centered in that width. */ + resize(); + size_t chars_w = SDL_max(w-16, new_w) / 8; + for (size_t iter = 0; iter < lines.size(); iter++) + { + size_t n_glyphs = utf8::unchecked::distance(lines[iter].begin(), lines[iter].end()); + signed int padding_needed = chars_w - n_glyphs; + if (padding_needed < 0) + { + continue; + } + size_t left_pad = padding_needed / 2; + size_t right_pad = padding_needed - left_pad; + + lines[iter] = std::string(left_pad, ' ') + lines[iter] + std::string(right_pad, ' '); + } + resize(); +} + +void textboxclass::centertext() +{ + padtowidth(w-16); +} diff --git a/desktop_version/src/Textbox.h b/desktop_version/src/Textbox.h index c2934958..b5bbf3f0 100644 --- a/desktop_version/src/Textbox.h +++ b/desktop_version/src/Textbox.h @@ -26,6 +26,12 @@ public: void resize(void); void addline(const std::string& t); + + void pad(size_t left_pad, size_t right_pad); + + void padtowidth(size_t new_w); + + void centertext(); public: //Fundamentals std::vector lines; diff --git a/desktop_version/src/XMLUtils.h b/desktop_version/src/XMLUtils.h index b56f2361..d1832bd0 100644 --- a/desktop_version/src/XMLUtils.h +++ b/desktop_version/src/XMLUtils.h @@ -27,3 +27,31 @@ tinyxml2::XMLDeclaration* update_declaration(tinyxml2::XMLDocument& doc); tinyxml2::XMLComment* update_comment(tinyxml2::XMLNode* parent, const char* text); } // namespace xml + + +// XMLHandle doc, XMLElement* elem +#define FOR_EACH_XML_ELEMENT(doc, elem) \ + for ( \ + elem = doc \ + .FirstChildElement() \ + .FirstChildElement() \ + .ToElement(); \ + elem != NULL; \ + elem = elem->NextSiblingElement() \ + ) + +// XMLElement* elem, XMLElement* subelem +#define FOR_EACH_XML_SUB_ELEMENT(elem, subelem) \ + for ( \ + subelem = elem->FirstChildElement(); \ + subelem != NULL; \ + subelem = subelem->NextSiblingElement() \ + ) + +// XMLElement* elem, const char* expect +#define EXPECT_ELEM(elem, expect) \ + if (SDL_strcmp(elem->Value(), expect) != 0) \ + { \ + continue; \ + } \ + do { } while (false) diff --git a/desktop_version/src/main.cpp b/desktop_version/src/main.cpp index f6a5a44b..d85ebb6c 100644 --- a/desktop_version/src/main.cpp +++ b/desktop_version/src/main.cpp @@ -16,6 +16,8 @@ #include "Input.h" #include "InterimVersion.h" #include "KeyPoll.h" +#include "Localization.h" +#include "LocalizationStorage.h" #include "Logic.h" #include "Map.h" #include "Music.h" @@ -368,6 +370,8 @@ int main(int argc, char *argv[]) { char* baseDir = NULL; char* assetsPath = NULL; + char* langDir = NULL; + char* fontsDir = NULL; bool seed_use_sdl_getticks = false; #ifdef _WIN32 bool open_console = false; @@ -420,6 +424,20 @@ int main(int argc, char *argv[]) assetsPath = argv[i]; }) } + else if (ARG("-langdir")) + { + ARG_INNER({ + i++; + langDir = argv[i]; + }) + } + else if (ARG("-fontsdir")) + { + ARG_INNER({ + i++; + fontsDir = argv[i]; + }) + } else if (ARG("-playing") || ARG("-p")) { ARG_INNER({ @@ -482,6 +500,10 @@ int main(int argc, char *argv[]) { vlog_toggle_error(0); } + else if (ARG("-translator")) + { + loc::show_translator_menu = true; + } #ifdef _WIN32 else if (ARG("-console")) { @@ -501,6 +523,10 @@ int main(int argc, char *argv[]) } } +#if defined(ALWAYS_SHOW_TRANSLATOR_MENU) + loc::show_translator_menu = true; +#endif + #ifdef _WIN32 if (open_console) { @@ -508,7 +534,7 @@ int main(int argc, char *argv[]) } #endif - if(!FILESYSTEM_init(argv[0], baseDir, assetsPath)) + if(!FILESYSTEM_init(argv[0], baseDir, assetsPath, langDir, fontsDir)) { vlog_error("Unable to initialize filesystem!"); VVV_exit(1); @@ -608,12 +634,24 @@ int main(int argc, char *argv[]) gameScreen.init(&screen_settings); } + loc::loadtext(false); + loc::loadlanguagelist(); + game.createmenu(Menu::mainmenu); + graphics.create_buffers(gameScreen.GetFormat()); if (game.skipfakeload) game.gamestate = TITLEMODE; if (game.slowdown == 0) game.slowdown = 30; + if (!loc::lang_set) + { + game.gamestate = TITLEMODE; + game.menustart = true; + game.createmenu(Menu::language); + game.currentmenuoption = loc::languagelist_curlang; + } + //Check to see if you've already unlocked some achievements here from before the update if (game.swnbestrank > 0){ if(game.swnbestrank >= 1) game.unlockAchievement("vvvvvvsupgrav5"); @@ -757,6 +795,7 @@ static void cleanup(void) music.destroy(); map.destroy(); NETWORK_shutdown(); + loc::resettext(true); SDL_Quit(); FILESYSTEM_deinit(); }