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(); }