1
0
mirror of https://github.com/TerryCavanagh/VVVVVV.git synced 2024-06-01 18:43:33 +02:00

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
This commit is contained in:
Dav999-v 2022-12-30 22:57:24 +01:00 committed by Misa Elizabeth Kai
parent 35d92e8e64
commit ec611ffa9d
22 changed files with 1567 additions and 95 deletions

View File

@ -83,6 +83,9 @@ set(VVV_SRC
src/Input.cpp src/Input.cpp
src/KeyPoll.cpp src/KeyPoll.cpp
src/Labclass.cpp src/Labclass.cpp
src/Localization.cpp
src/LocalizationMaint.cpp
src/LocalizationStorage.cpp
src/Logic.cpp src/Logic.cpp
src/Map.cpp src/Map.cpp
src/Music.cpp src/Music.cpp
@ -90,6 +93,7 @@ set(VVV_SRC
src/preloader.cpp src/preloader.cpp
src/Render.cpp src/Render.cpp
src/RenderFixed.cpp src/RenderFixed.cpp
src/RoomnameTranslator.cpp
src/Screen.cpp src/Screen.cpp
src/Script.cpp src/Script.cpp
src/Scripts.cpp src/Scripts.cpp

View File

@ -18,6 +18,8 @@
#include "Graphics.h" #include "Graphics.h"
#include "GraphicsUtil.h" #include "GraphicsUtil.h"
#include "KeyPoll.h" #include "KeyPoll.h"
#include "Localization.h"
#include "LocalizationStorage.h"
#include "Map.h" #include "Map.h"
#include "Script.h" #include "Script.h"
#include "UtilityClass.h" #include "UtilityClass.h"
@ -81,6 +83,24 @@ static bool compare_nocase (std::string first, std::string second)
return false; 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) static void levelZipCallback(const char* filename)
{ {
if (!FILESYSTEM_isFile(filename)) if (!FILESYSTEM_isFile(filename))
@ -228,6 +248,9 @@ static void levelMetaDataCallback(const char* filename)
if (cl.getLevelMetaData(filename_, temp)) if (cl.getLevelMetaData(filename_, temp))
{ {
temp.title = translate_title(temp.title);
temp.creator = translate_creator(temp.creator);
cl.ListOfMetaData.push_back(temp); cl.ListOfMetaData.push_back(temp);
} }
} }
@ -318,7 +341,7 @@ void customlevelclass::reset(void)
mapwidth=5; mapwidth=5;
mapheight=5; mapheight=5;
title="Untitled Level"; title="Untitled Level"; // Already translatable
creator="Unknown"; creator="Unknown";
levmusic=0; levmusic=0;
@ -1288,6 +1311,8 @@ next:
ed.gethooks(); ed.gethooks();
#endif #endif
loc::loadtext_custom(_path.c_str());
version=2; version=2;
return true; return true;

View File

@ -160,6 +160,10 @@ public:
bool onewaycol_override; bool onewaycol_override;
}; };
std::string translate_title(const std::string& title);
std::string translate_creator(const std::string& creator);
#ifndef CL_DEFINITION #ifndef CL_DEFINITION
extern customlevelclass cl; extern customlevelclass cl;
#endif #endif

View File

@ -1220,7 +1220,7 @@ void editorrender(void)
if(tb>255) tb=255; if(tb>255) tb=255;
editormenurender(tr, tg, tb); editormenurender(tr, tg, tb);
graphics.drawmenu(tr, tg, tb); graphics.drawmenu(tr, tg, tb, game.currentmenuname);
} }
else if (ed.textmod) else if (ed.textmod)
{ {

View File

@ -6,12 +6,15 @@
#include "Alloc.h" #include "Alloc.h"
#include "BinaryBlob.h" #include "BinaryBlob.h"
#include "Constants.h"
#include "Exit.h" #include "Exit.h"
#include "Graphics.h" #include "Graphics.h"
#include "Localization.h"
#include "Maths.h" #include "Maths.h"
#include "Screen.h" #include "Screen.h"
#include "Unused.h" #include "Unused.h"
#include "UtilityClass.h" #include "UtilityClass.h"
#include "VFormat.h"
#include "Vlogging.h" #include "Vlogging.h"
/* These are needed for PLATFORM_* crap */ /* These are needed for PLATFORM_* crap */
@ -40,8 +43,11 @@ static bool isInit = false;
static const char* pathSep = NULL; static const char* pathSep = NULL;
static char* basePath = NULL; static char* basePath = NULL;
static char writeDir[MAX_PATH] = {'\0'};
static char saveDir[MAX_PATH] = {'\0'}; static char saveDir[MAX_PATH] = {'\0'};
static char levelDir[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 assetDir[MAX_PATH] = {'\0'};
static char virtualMountPath[MAX_PATH] = {'\0'}; static char virtualMountPath[MAX_PATH] = {'\0'};
@ -66,7 +72,105 @@ static const PHYSFS_Allocator allocator = {
SDL_free 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]; char output[MAX_PATH];
@ -102,29 +206,30 @@ int FILESYSTEM_init(char *argvZero, char* baseDir, char *assetsPath)
} }
/* Mount our base user directory */ /* 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( vlog_error(
"Could not mount %s: %s", "Could not mount %s: %s",
output, writeDir,
PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode()) PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode())
); );
return 0; return 0;
} }
if (!PHYSFS_setWriteDir(output)) if (!PHYSFS_setWriteDir(writeDir))
{ {
vlog_error( vlog_error(
"Could not set write dir to %s: %s", "Could not set write dir to %s: %s",
output, writeDir,
PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode()) PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode())
); );
return 0; return 0;
} }
vlog_info("Base directory: %s", output); vlog_info("Base directory: %s", writeDir);
/* Store full save directory */ /* Store full save directory */
SDL_snprintf(saveDir, sizeof(saveDir), "%s%s%s", SDL_snprintf(saveDir, sizeof(saveDir), "%s%s%s",
output, writeDir,
"saves", "saves",
pathSep pathSep
); );
@ -133,7 +238,7 @@ int FILESYSTEM_init(char *argvZero, char* baseDir, char *assetsPath)
/* Store full level directory */ /* Store full level directory */
SDL_snprintf(levelDir, sizeof(levelDir), "%s%s%s", SDL_snprintf(levelDir, sizeof(levelDir), "%s%s%s",
output, writeDir,
"levels", "levels",
pathSep pathSep
); );
@ -148,6 +253,11 @@ int FILESYSTEM_init(char *argvZero, char* baseDir, char *assetsPath)
basePath = SDL_strdup("./"); 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 */ /* Mount the stock content last */
if (assetsPath) if (assetsPath)
{ {
@ -217,7 +327,40 @@ char *FILESYSTEM_getUserLevelDirectory(void)
return levelDir; 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; PHYSFS_Stat stat;
@ -235,10 +378,20 @@ bool FILESYSTEM_isFile(const char* filename)
/* We unfortunately cannot follow symlinks (PhysFS limitation). /* We unfortunately cannot follow symlinks (PhysFS limitation).
* Let the caller deal with them. * Let the caller deal with them.
*/ */
return stat.filetype == PHYSFS_FILETYPE_REGULAR return stat.filetype == filetype
|| stat.filetype == PHYSFS_FILETYPE_SYMLINK; || 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) bool FILESYSTEM_isMounted(const char* filename)
{ {
return PHYSFS_getMountPoint(filename) != NULL; 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; static bool levelDirHasError = false;
@ -501,6 +654,31 @@ bool FILESYSTEM_isAssetMounted(const char* filename)
return SDL_strcmp(assetDir, realDir) == 0; 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) static void load_stdin(void)
{ {
size_t pos = 0; size_t pos = 0;
@ -809,6 +987,20 @@ bool FILESYSTEM_loadTiXml2Document(const char *name, tinyxml2::XMLDocument& doc)
return true; 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 struct CallbackWrapper
{ {
void (*callback)(const char* filename); void (*callback)(const char* filename);
@ -853,6 +1045,28 @@ void FILESYSTEM_enumerateLevelDirFileNames(
} }
} }
std::vector<std::string> FILESYSTEM_getLanguageCodes(void)
{
std::vector<std::string> 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) static int PLATFORM_getOSDirectory(char* output, const size_t output_size)
{ {
#ifdef _WIN32 #ifdef _WIN32

View File

@ -5,16 +5,23 @@
class binaryBlob; class binaryBlob;
#include <stddef.h> #include <stddef.h>
#include <string>
#include <vector>
// Forward declaration, including the entirety of tinyxml2.h across all files this file is included in is unnecessary // Forward declaration, including the entirety of tinyxml2.h across all files this file is included in is unnecessary
namespace tinyxml2 { class XMLDocument; } 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); bool FILESYSTEM_isInit(void);
void FILESYSTEM_deinit(void); void FILESYSTEM_deinit(void);
char *FILESYSTEM_getUserSaveDirectory(void); char *FILESYSTEM_getUserSaveDirectory(void);
char *FILESYSTEM_getUserLevelDirectory(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_isFile(const char* filename);
bool FILESYSTEM_isMounted(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); bool FILESYSTEM_mountAssets(const char *path);
void FILESYSTEM_unmountAssets(void); void FILESYSTEM_unmountAssets(void);
bool FILESYSTEM_isAssetMounted(const char* filename); bool FILESYSTEM_isAssetMounted(const char* filename);
bool FILESYSTEM_areAssetsInSameRealDir(const char* filenameA, const char* filenameB);
void FILESYSTEM_loadFileToMemory(const char *name, unsigned char **mem, void FILESYSTEM_loadFileToMemory(const char *name, unsigned char **mem,
size_t *len, bool addnull); 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_saveTiXml2Document(const char *name, tinyxml2::XMLDocument& doc, bool sync = true);
bool FILESYSTEM_loadTiXml2Document(const char *name, tinyxml2::XMLDocument& doc); 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)); void FILESYSTEM_enumerateLevelDirFileNames(void (*callback)(const char* filename));
std::vector<std::string> FILESYSTEM_getLanguageCodes(void);
bool FILESYSTEM_levelDirHasError(void); bool FILESYSTEM_levelDirHasError(void);
void FILESYSTEM_clearLevelDirError(void); void FILESYSTEM_clearLevelDirError(void);
const char* FILESYSTEM_getLevelDirError(void); const char* FILESYSTEM_getLevelDirError(void);

View File

@ -15,14 +15,18 @@
#include "FileSystemUtils.h" #include "FileSystemUtils.h"
#include "GlitchrunnerMode.h" #include "GlitchrunnerMode.h"
#include "Graphics.h" #include "Graphics.h"
#include "Localization.h"
#include "LocalizationStorage.h"
#include "KeyPoll.h" #include "KeyPoll.h"
#include "MakeAndPlay.h" #include "MakeAndPlay.h"
#include "Map.h" #include "Map.h"
#include "Music.h" #include "Music.h"
#include "Network.h" #include "Network.h"
#include "RoomnameTranslator.h"
#include "Screen.h" #include "Screen.h"
#include "Script.h" #include "Script.h"
#include "UtilityClass.h" #include "UtilityClass.h"
#include "VFormat.h"
#include "Vlogging.h" #include "Vlogging.h"
#include "XMLUtils.h" #include "XMLUtils.h"
@ -262,13 +266,13 @@ void Game::init(void)
SDL_memset(unlocknotify, false, sizeof(unlock)); SDL_memset(unlocknotify, false, sizeof(unlock));
currentmenuoption = 0; currentmenuoption = 0;
menutestmode = false;
current_credits_list_index = 0; current_credits_list_index = 0;
menuxoff = 0; menuxoff = 0;
menuyoff = 0; menuyoff = 0;
menucountdown = 0; menucountdown = 0;
levelpage=0; levelpage=0;
playcustomlevel=0; playcustomlevel=0;
createmenu(Menu::mainmenu);
silence_settings_error = false; silence_settings_error = false;
@ -288,6 +292,7 @@ void Game::init(void)
timetrialshinytarget = 0; timetrialshinytarget = 0;
timetrialparlost = false; timetrialparlost = false;
timetrialpar = 0; timetrialpar = 0;
timetrialcheater = false;
timetrialresulttime = 0; timetrialresulttime = 0;
timetrialresultframes = 0; timetrialresultframes = 0;
timetrialresultshinytarget = 0; timetrialresultshinytarget = 0;
@ -1455,6 +1460,11 @@ void Game::updatestate(void)
obj.removetrigger(82); obj.removetrigger(82);
hascontrol = false; hascontrol = false;
if (timetrialcheater)
{
SDL_zeroa(obj.collect);
}
timetrialresulttime = help.hms_to_seconds(hours, minutes, seconds); timetrialresulttime = help.hms_to_seconds(hours, minutes, seconds);
timetrialresultframes = frames; timetrialresultframes = frames;
timetrialresulttrinkets = trinkets(); timetrialresulttrinkets = trinkets();
@ -4379,6 +4389,21 @@ void Game::deserializesettings(tinyxml2::XMLElement* dataNode, struct ScreenSett
key.sensitivity = help.Int(pText); 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) 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, "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; static bool settings_loaded = false;
@ -5281,6 +5310,14 @@ void Game::customloadquick(const std::string& savfile)
music.play(song); 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) else if (SDL_strcmp(pKey, "showminimap") == 0)
{ {
map.customshowmm = help.Int(pText); 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, "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, "teleportscript", teleportscript.c_str());
xml::update_tag(msgs, "companion", companion); xml::update_tag(msgs, "companion", companion);
@ -5792,6 +5831,7 @@ void Game::loadtele(void)
std::string Game::unrescued(void) std::string Game::unrescued(void)
{ {
//Randomly return the name of an unrescued crewmate //Randomly return the name of an unrescued crewmate
//Localization is handled with regular cutscene dialogue
if (fRandom() * 100 > 50) if (fRandom() * 100 > 50)
{ {
if (!crewstats[5]) return "Victoria"; 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*/ ) 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 //Either we've just booted up the game or returned from gamemode
//Whichever it is, we shouldn't have a stack, //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"); option("levels");
#endif #endif
option("options"); option("options");
if (loc::show_translator_menu)
{
option(loc::gettext("translator"));
}
#if !defined(MAKEANDPLAY) #if !defined(MAKEANDPLAY)
option("credits"); option("credits");
#endif #endif
@ -6259,6 +6303,66 @@ void Game::createmenu( enum Menu::MenuName t, bool samemenu/*= false*/ )
menuyoff = 0; menuyoff = 0;
maxspacing = 10; maxspacing = 10;
break; 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::cleardatamenu:
case Menu::clearcustomdatamenu: case Menu::clearcustomdatamenu:
option("no! don't delete"); option("no! don't delete");
@ -6773,6 +6877,7 @@ void Game::quittomenu(void)
gamestate = TITLEMODE; gamestate = TITLEMODE;
graphics.fademode = FADE_START_FADEIN; graphics.fademode = FADE_START_FADEIN;
FILESYSTEM_unmountAssets(); FILESYSTEM_unmountAssets();
loc::unloadtext_custom();
cliplaytest = false; cliplaytest = false;
graphics.titlebg.tdrawback = true; graphics.titlebg.tdrawback = true;
graphics.flipmode = false; 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) bool Game::incompetitive(void)
{ {
return ( return (
@ -7031,6 +7141,19 @@ bool Game::nocompetitive(void)
return slowdown < 30 || map.invincibility; 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() bool Game::isingamecompletescreen()
{ {
return (state >= 3501 && state <= 3518) || (state >= 3520 && state <= 3522); return (state >= 3501 && state <= 3518) || (state >= 3520 && state <= 3522);

View File

@ -60,6 +60,14 @@ namespace Menu
audiooptions, audiooptions,
accessibility, accessibility,
controller, controller,
language,
translator_main,
translator_options,
translator_options_limitscheck,
translator_options_stats,
translator_maintenance,
translator_maintenance_sync,
translator_error_setlangwritedir,
cleardatamenu, cleardatamenu,
clearcustomdatamenu, clearcustomdatamenu,
setinvincibility, setinvincibility,
@ -290,6 +298,7 @@ public:
//Main Menu Variables //Main Menu Variables
std::vector<MenuOption> menuoptions; std::vector<MenuOption> menuoptions;
int currentmenuoption ; int currentmenuoption ;
bool menutestmode;
enum Menu::MenuName currentmenuname; enum Menu::MenuName currentmenuname;
enum Menu::MenuName kludge_ingametemp; enum Menu::MenuName kludge_ingametemp;
enum SLIDERMODE slidermode; enum SLIDERMODE slidermode;
@ -330,6 +339,7 @@ public:
bool noflashingmode; bool noflashingmode;
int slowdown; int slowdown;
int get_timestep(void); int get_timestep(void);
bool physics_frozen(void);
bool nodeathmode; bool nodeathmode;
int gameoverdelay; int gameoverdelay;
@ -343,6 +353,7 @@ public:
bool intimetrial, timetrialparlost; bool intimetrial, timetrialparlost;
int timetrialcountdown, timetrialshinytarget, timetriallevel; int timetrialcountdown, timetrialshinytarget, timetriallevel;
int timetrialpar, timetrialresulttime, timetrialresultframes, timetrialrank; int timetrialpar, timetrialresulttime, timetrialresultframes, timetrialrank;
bool timetrialcheater;
int timetrialresultshinytarget, timetrialresulttrinkets, timetrialresultpar; int timetrialresultshinytarget, timetrialresulttrinkets, timetrialresultpar;
int timetrialresultdeaths; int timetrialresultdeaths;
@ -423,7 +434,7 @@ public:
//Some stats: //Some stats:
int totalflips; 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; int hardestroomdeaths, currentroomdeaths;
@ -483,6 +494,9 @@ public:
bool incompetitive(void); bool incompetitive(void);
bool nocompetitive(void); bool nocompetitive(void);
bool nocompetitive_unless_translator(void);
void sabotage_time_trial(void);
bool over30mode; bool over30mode;
bool showingametimer; bool showingametimer;

View File

@ -11,10 +11,13 @@
#include "Exit.h" #include "Exit.h"
#include "FileSystemUtils.h" #include "FileSystemUtils.h"
#include "GraphicsUtil.h" #include "GraphicsUtil.h"
#include "Localization.h"
#include "Map.h" #include "Map.h"
#include "Music.h" #include "Music.h"
#include "RoomnameTranslator.h"
#include "Screen.h" #include "Screen.h"
#include "UtilityClass.h" #include "UtilityClass.h"
#include "VFormat.h"
#include "Vlogging.h" #include "Vlogging.h"
void Graphics::init(void) void Graphics::init(void)
@ -150,6 +153,11 @@ void Graphics::init(void)
minimap_mounted = false; minimap_mounted = false;
#endif #endif
gamecomplete_mounted = false;
levelcomplete_mounted = false;
flipgamecomplete_mounted = false;
fliplevelcomplete_mounted = false;
SDL_zeroa(error); SDL_zeroa(error);
SDL_zeroa(error_title); SDL_zeroa(error_title);
} }
@ -193,6 +201,11 @@ void Graphics::create_buffers(const SDL_PixelFormat* fmt)
SDL_SetSurfaceAlphaMod(footerbuffer, 127); SDL_SetSurfaceAlphaMod(footerbuffer, 127);
FillRect(footerbuffer, SDL_MapRGB(fmt, 0, 0, 0)); 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); ghostbuffer = CREATE_SURFACE(320, 240);
SDL_SetSurfaceBlendMode(ghostbuffer, SDL_BLENDMODE_BLEND); SDL_SetSurfaceBlendMode(ghostbuffer, SDL_BLENDMODE_BLEND);
SDL_SetSurfaceAlphaMod(ghostbuffer, 127); SDL_SetSurfaceAlphaMod(ghostbuffer, 127);
@ -233,6 +246,7 @@ void Graphics::destroy_buffers(void)
FREE_SURFACE(backBuffer); FREE_SURFACE(backBuffer);
FREE_SURFACE(footerbuffer); FREE_SURFACE(footerbuffer);
FREE_SURFACE(roomname_translator::dimbuffer);
FREE_SURFACE(ghostbuffer); FREE_SURFACE(ghostbuffer);
FREE_SURFACE(foregroundBuffer); FREE_SURFACE(foregroundBuffer);
FREE_SURFACE(menubuffer); FREE_SURFACE(menubuffer);
@ -360,9 +374,12 @@ bool Graphics::Makebfont(void)
flipbfont.push_back(TempFlipped); flipbfont.push_back(TempFlipped);
}) })
unsigned char* charmap; unsigned char* charmap = NULL;
size_t length; 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) if (charmap != NULL)
{ {
unsigned char* current = charmap; unsigned char* current = charmap;
@ -584,10 +601,14 @@ bool Graphics::next_wrap(
switch (str[idx]) switch (str[idx])
{ {
case ' ': case ' ':
lenfromlastspace = idx; if (loc::get_langmeta()->autowordwrap)
lastspace = *start; {
lenfromlastspace = idx;
lastspace = *start;
}
break; break;
case '\n': case '\n':
case '|':
*start += 1; *start += 1;
SDL_FALLTHROUGH; SDL_FALLTHROUGH;
case '\0': case '\0':
@ -634,17 +655,29 @@ bool Graphics::next_wrap_s(
return retval; return retval;
} }
void Graphics::PrintWrap( int Graphics::PrintWrap(
const int x, const int x,
int y, int y,
const char* str, std::string s,
const int r, const int r,
const int g, const int g,
const int b, const int b,
const bool cen, const bool cen /*= false*/,
const int linespacing, int linespacing /*= -1*/,
const int maxwidth 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. /* 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. */ * 320 / 6 is 54, rounded up. 4 bytes per char. */
char buffer[54*4 + 1]; char buffer[54*4 + 1];
@ -675,6 +708,8 @@ void Graphics::PrintWrap(
y += linespacing; y += linespacing;
} }
} }
return y + linespacing;
} }
@ -723,6 +758,135 @@ int Graphics::len(const std::string& t)
return bfontpos; 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<std::string> 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*/ ) { 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); bprintalpha(x,y,t,r,g,b,255,cen);
} }
@ -1520,8 +1684,19 @@ void Graphics::setfade(const int amount)
oldfadeamount = 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++) for (size_t i = 0; i < game.menuoptions.size(); i++)
{ {
MenuOption& opt = game.menuoptions[i]; MenuOption& opt = game.menuoptions[i];
@ -1542,11 +1717,21 @@ void Graphics::drawmenu( int cr, int cg, int cb, bool levelmenu /*= false*/ )
fb = 128; fb = 128;
} }
int x = i*game.menuspacing + game.menuxoff; int x, y;
int y = 140 + i*12 + game.menuyoff; 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 #ifndef NO_CUSTOM_LEVELS
if (levelmenu) if (menu == Menu::levellist)
{ {
size_t separator; size_t separator;
if (cl.ListOfMetaData.size() > 8) if (cl.ListOfMetaData.size() > 8)
@ -1570,31 +1755,28 @@ void Graphics::drawmenu( int cr, int cg, int cb, bool levelmenu /*= false*/ )
} }
#endif #endif
char tempstring[MENU_TEXT_BYTES];
SDL_strlcpy(tempstring, opt.text, sizeof(tempstring));
char buffer[MENU_TEXT_BYTES]; char buffer[MENU_TEXT_BYTES];
if ((int) i == game.currentmenuoption && game.slidermode == SLIDER_NONE) if ((int) i == game.currentmenuoption && game.slidermode == SLIDER_NONE)
{ {
std::string opt_text;
if (opt.active) if (opt.active)
{ {
// Uppercase the text // Uppercase the text
// FIXME: This isn't UTF-8 aware! opt_text = loc::toupper(opt.text);
size_t templen = SDL_strlen(tempstring); }
for (size_t ii = 0; ii < templen; ii++) else
{ {
tempstring[ii] = SDL_toupper(tempstring[ii]); opt_text = loc::remove_toupper_escape_chars(opt.text);
}
} }
// Add brackets vformat_buf(buffer, sizeof(buffer), loc::get_langmeta()->menu_select.c_str(), "label:str", opt_text.c_str());
SDL_snprintf(buffer, sizeof(buffer), "[ %s ]", tempstring);
// Account for brackets // Account for brackets
x -= 16; x -= (len(buffer)-len(opt_text))/2;
} }
else 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); Print(x, y, buffer, fr, fg, fb);
@ -3112,6 +3294,84 @@ void Graphics::textboxcentery(void)
textboxes[m].centery(); 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) int Graphics::crewcolour(const int t)
{ {
//given crewmate t, return colour in setcol //given crewmate t, return colour in setcol
@ -3417,6 +3677,11 @@ bool Graphics::reloadresources(void)
minimap_mounted = FILESYSTEM_isAssetMounted("graphics/minimap.png"); minimap_mounted = FILESYSTEM_isAssetMounted("graphics/minimap.png");
#endif #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; return true;
fail: fail:
@ -3442,3 +3707,18 @@ Uint32 Graphics::crewcolourreal(int t)
} }
return col_crewcyan; 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);
}
}

View File

@ -5,6 +5,7 @@
#include <string> #include <string>
#include <vector> #include <vector>
#include "Game.h"
#include "GraphicsResources.h" #include "GraphicsResources.h"
#include "GraphicsUtil.h" #include "GraphicsUtil.h"
#include "Maths.h" #include "Maths.h"
@ -53,7 +54,7 @@ public:
void drawcoloredtile(int x, int y, int t, int r, int g, int b); 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 processfade(void);
void setfade(const int amount); void setfade(const int amount);
@ -96,6 +97,16 @@ public:
void textboxcentery(void); 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 textboxadjust(void);
void addline(const std::string& t); 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); 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 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); 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); 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 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 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); void drawspritesetcol(int x, int y, int t, int c);
@ -235,6 +250,11 @@ public:
bool minimap_mounted; bool minimap_mounted;
#endif #endif
bool gamecomplete_mounted;
bool levelcomplete_mounted;
bool flipgamecomplete_mounted;
bool fliplevelcomplete_mounted;
void menuoffrender(void); void menuoffrender(void);
@ -335,10 +355,16 @@ public:
SDL_Surface* ghostbuffer; SDL_Surface* ghostbuffer;
#ifndef GAME_DEFINITION
float inline lerp(const float v0, const float v1) float inline lerp(const float v0, const float v1)
{ {
if (game.physics_frozen())
{
return v1;
}
return v0 + alpha * (v1 - v0); return v0 + alpha * (v1 - v0);
} }
#endif
float alpha; float alpha;
Uint32 col_crewred; Uint32 col_crewred;
@ -359,6 +385,8 @@ public:
Uint32 crewcolourreal(int t); Uint32 crewcolourreal(int t);
void render_roomname(const char* roomname, int r, int g, int b);
char error[128]; char error[128];
char error_title[128]; /* for SDL_ShowSimpleMessageBox */ char error_title[128]; /* for SDL_ShowSimpleMessageBox */
}; };

View File

@ -10,9 +10,13 @@
#include "GlitchrunnerMode.h" #include "GlitchrunnerMode.h"
#include "Graphics.h" #include "Graphics.h"
#include "KeyPoll.h" #include "KeyPoll.h"
#include "Localization.h"
#include "LocalizationMaint.h"
#include "LocalizationStorage.h"
#include "MakeAndPlay.h" #include "MakeAndPlay.h"
#include "Map.h" #include "Map.h"
#include "Music.h" #include "Music.h"
#include "RoomnameTranslator.h"
#include "Screen.h" #include "Screen.h"
#include "Script.h" #include "Script.h"
#include "UtilityClass.h" #include "UtilityClass.h"
@ -361,24 +365,47 @@ static void slidermodeinput(void)
static void menuactionpress(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) switch (game.currentmenuname)
{ {
case Menu::mainmenu: case Menu::mainmenu:
#if defined(MAKEANDPLAY) {
#define MPOFFSET -1 int option_id = -1;
#else int option_seq = 0; /* option number in YOUR configuration */
#define MPOFFSET 0 #define OPTION_ID(id) \
if (option_seq == game.currentmenuoption) \
{ \
option_id = id; \
} \
option_seq++;
#if !defined(MAKEANDPLAY)
OPTION_ID(0) /* play */
#endif #endif
#if !defined(NO_CUSTOM_LEVELS)
#if defined(NO_CUSTOM_LEVELS) OPTION_ID(1) /* levels */
#define NOCUSTOMSOFFSET -1
#else
#define NOCUSTOMSOFFSET 0
#endif #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) #if !defined(MAKEANDPLAY)
case 0: case 0:
@ -399,40 +426,41 @@ static void menuactionpress(void)
break; break;
#endif #endif
#if !defined(NO_CUSTOM_LEVELS) #if !defined(NO_CUSTOM_LEVELS)
case OFFSET+1: case 1:
//Bring you to the normal playmenu //Bring you to the normal playmenu
music.playef(11); music.playef(11);
game.createmenu(Menu::playerworlds); game.createmenu(Menu::playerworlds);
map.nexttowercolour(); map.nexttowercolour();
break; break;
#endif #endif
case OFFSET+2: case 2:
//Options //Options
music.playef(11); music.playef(11);
game.createmenu(Menu::options); game.createmenu(Menu::options);
map.nexttowercolour(); map.nexttowercolour();
break; break;
case 3:
//Translator
music.playef(11);
game.createmenu(Menu::translator_main);
map.nexttowercolour();
break;
#if !defined(MAKEANDPLAY) #if !defined(MAKEANDPLAY)
case OFFSET+3: case 4:
//Credits //Credits
music.playef(11); music.playef(11);
game.createmenu(Menu::credits); game.createmenu(Menu::credits);
map.nexttowercolour(); map.nexttowercolour();
break; break;
#else
#undef MPOFFSET
#define MPOFFSET -2
#endif #endif
case OFFSET+4: case 5:
music.playef(11); music.playef(11);
game.createmenu(Menu::youwannaquit); game.createmenu(Menu::youwannaquit);
map.nexttowercolour(); map.nexttowercolour();
break; break;
#undef OFFSET
#undef NOCUSTOMSOFFSET
#undef MPOFFSET
} }
break; break;
}
#if !defined(NO_CUSTOM_LEVELS) #if !defined(NO_CUSTOM_LEVELS)
case Menu::levellist: case Menu::levellist:
{ {
@ -1010,6 +1038,14 @@ static void menuactionpress(void)
game.createmenu(Menu::accessibility); game.createmenu(Menu::accessibility);
map.nexttowercolour(); map.nexttowercolour();
break; break;
case 5:
//language options
music.playef(11);
loc::loadlanguagelist();
game.createmenu(Menu::language);
game.currentmenuoption = loc::languagelist_curlang;
map.nexttowercolour();
break;
default: default:
/* Return */ /* Return */
music.playef(11); music.playef(11);
@ -1065,6 +1101,181 @@ static void menuactionpress(void)
music.playef(11); music.playef(11);
} }
break; 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: case Menu::unlockmenutrials:
switch (game.currentmenuoption) switch (game.currentmenuoption)
{ {
@ -1561,7 +1772,7 @@ static void menuactionpress(void)
map.nexttowercolour(); map.nexttowercolour();
break; break;
case Menu::playmodes: 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); music.playef(11);
game.createmenu(Menu::timetrials); game.createmenu(Menu::timetrials);
@ -1828,11 +2039,30 @@ void titleinput(void)
game.press_map = false; game.press_map = false;
game.press_interact = false; game.press_interact = false;
bool lang_press_horizontal = false;
if (graphics.flipmode) 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_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; 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 else
{ {
if (key.isDown(KEYBOARD_LEFT) || key.isDown(KEYBOARD_UP) || key.isDown(KEYBOARD_a) || key.isDown(KEYBOARD_w) || key.controllerWantsLeft(true)) 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 && game.menucountdown <= 0
&& (key.isDown(27) || key.isDown(game.controllerButton_esc))) && (key.isDown(27) || key.isDown(game.controllerButton_esc)))
{ {
music.playef(11); if (game.currentmenuname == Menu::language && !loc::lang_set)
if (game.currentmenuname == Menu::mainmenu) {
/* 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); game.createmenu(Menu::youwannaquit);
map.nexttowercolour(); map.nexttowercolour();
@ -1908,7 +2153,63 @@ void titleinput(void)
{ {
if (game.slidermode == SLIDER_NONE) 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--; game.currentmenuoption--;
} }
@ -1967,6 +2268,11 @@ void gameinput(void)
if(!script.running) if(!script.running)
{ {
if (roomname_translator::enabled && roomname_translator::overlay_input())
{
return;
}
game.press_left = false; game.press_left = false;
game.press_right = false; game.press_right = false;
game.press_action = false; game.press_action = false;

View File

@ -9,6 +9,8 @@
#include "Game.h" #include "Game.h"
#include "GlitchrunnerMode.h" #include "GlitchrunnerMode.h"
#include "Graphics.h" #include "Graphics.h"
#include "Localization.h"
#include "LocalizationStorage.h"
#include "Music.h" #include "Music.h"
#include "Screen.h" #include "Screen.h"
#include "Vlogging.h" #include "Vlogging.h"
@ -165,6 +167,13 @@ void KeyPoll::Poll(void)
fullscreenkeybind = true; 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 (textentry())
{ {
if (evt.key.keysym.sym == SDLK_BACKSPACE && !keybuffer.empty()) if (evt.key.keysym.sym == SDLK_BACKSPACE && !keybuffer.empty())
@ -470,3 +479,13 @@ bool KeyPoll::controllerWantsRight(bool includeVert)
( buttonmap[SDL_CONTROLLER_BUTTON_DPAD_DOWN] || ( buttonmap[SDL_CONTROLLER_BUTTON_DPAD_DOWN] ||
yVel > 0 ) ) ); 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;
}

View File

@ -59,6 +59,8 @@ public:
bool controllerButtonDown(void); bool controllerButtonDown(void);
bool controllerWantsLeft(bool includeVert); bool controllerWantsLeft(bool includeVert);
bool controllerWantsRight(bool includeVert); bool controllerWantsRight(bool includeVert);
bool controllerWantsUp(void);
bool controllerWantsDown(void);
int leftbutton, rightbutton, middlebutton; int leftbutton, rightbutton, middlebutton;
int mx, my; int mx, my;

View File

@ -127,6 +127,11 @@ static void gotoroom_wrapper(const int rx, const int ry)
void gamelogic(void) void gamelogic(void)
{ {
if (game.physics_frozen())
{
return;
}
bool roomchange = false; bool roomchange = false;
#define GOTOROOM(rx, ry) \ #define GOTOROOM(rx, ry) \
gotoroom_wrapper(rx, ry); \ gotoroom_wrapper(rx, ry); \

View File

@ -11,14 +11,18 @@
#include "GraphicsUtil.h" #include "GraphicsUtil.h"
#include "InterimVersion.h" #include "InterimVersion.h"
#include "KeyPoll.h" #include "KeyPoll.h"
#include "Localization.h"
#include "LocalizationStorage.h"
#include "MakeAndPlay.h" #include "MakeAndPlay.h"
#include "Map.h" #include "Map.h"
#include "Maths.h" #include "Maths.h"
#include "Music.h" #include "Music.h"
#include "ReleaseVersion.h" #include "ReleaseVersion.h"
#include "RoomnameTranslator.h"
#include "Screen.h" #include "Screen.h"
#include "Script.h" #include "Script.h"
#include "UtilityClass.h" #include "UtilityClass.h"
#include "VFormat.h"
static int tr; static int tr;
static int tg; 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, 65, "Disable screen effects, enable", tr, tg, tb, true);
graphics.Print(-1, 75, "slowdown modes or invincibility.", tr, tg, tb, true); graphics.Print(-1, 75, "slowdown modes or invincibility.", tr, tg, tb, true);
break; 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; break;
case Menu::graphicoptions: 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; break;
case Menu::speedrunneroptions: case Menu::speedrunneroptions:
switch (game.currentmenuoption) switch (game.currentmenuoption)
@ -1460,7 +1647,7 @@ void titlerender(void)
if(tg>255) tg=255; if(tg>255) tg=255;
if (tb < 0) tb = 0; if (tb < 0) tb = 0;
if(tb>255) tb=255; if(tb>255) tb=255;
graphics.drawmenu(tr, tg, tb, game.currentmenuname == Menu::levellist); graphics.drawmenu(tr, tg, tb, game.currentmenuname);
} }
graphics.drawfade(); graphics.drawfade();
@ -1728,7 +1915,8 @@ void gamerender(void)
&& !game.intimetrial && !game.intimetrial
&& !game.isingamecompletescreen() && !game.isingamecompletescreen()
&& (!game.swnmode || game.swngame != 1) && (!game.swnmode || game.swngame != 1)
&& game.showingametimer) && game.showingametimer
&& !roomname_translator::enabled)
{ {
char buffer[SCREEN_WIDTH_CHARS + 1]; char buffer[SCREEN_WIDTH_CHARS + 1];
graphics.bprint(6, 6, "TIME:", 255,255,255); graphics.bprint(6, 6, "TIME:", 255,255,255);
@ -1736,31 +1924,30 @@ void gamerender(void)
graphics.bprint(46, 6, buffer, 196, 196, 196); 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; const char* roomname;
graphics.footerrect.y = 230;
if (map.finalmode) if (map.finalmode)
{ {
roomname = map.glitchname; roomname = loc::gettext_roomname(map.custommode, game.roomx, game.roomy, map.glitchname, map.roomname_special);
} }
else else
{ {
roomname = map.roomname; roomname = loc::gettext_roomname(map.custommode, game.roomx, game.roomy, map.roomname, map.roomname_special);
} }
if (graphics.translucentroomname) graphics.render_roomname(roomname, roomname_r, roomname_g, roomname_b);
{
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);
}
} }
if (map.roomtexton) 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); 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]; char buffer[SCREEN_WIDTH_CHARS + 1];
game.timestringcenti(buffer, sizeof(buffer)); game.timestringcenti(buffer, sizeof(buffer));
@ -2031,15 +2218,15 @@ static void draw_roomname_menu(void)
if (map.hiddenname[0] != '\0') if (map.hiddenname[0] != '\0')
{ {
name = map.hiddenname; name = loc::gettext_roomname_special(map.hiddenname);
} }
else if (map.finalmode) else if (map.finalmode)
{ {
name = map.glitchname; name = loc::gettext_roomname(map.custommode, game.roomx, game.roomy, map.glitchname, map.roomname_special);
} }
else 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); graphics.Print(5, 2, name, 196, 196, 255 - help.glow, true);

View File

@ -4,6 +4,7 @@
#include <limits.h> #include <limits.h>
#include <SDL_timer.h> #include <SDL_timer.h>
#include "Constants.h"
#include "CustomLevels.h" #include "CustomLevels.h"
#include "Editor.h" #include "Editor.h"
#include "Entity.h" #include "Entity.h"
@ -12,10 +13,13 @@
#include "GlitchrunnerMode.h" #include "GlitchrunnerMode.h"
#include "Graphics.h" #include "Graphics.h"
#include "KeyPoll.h" #include "KeyPoll.h"
#include "Localization.h"
#include "LocalizationStorage.h"
#include "Map.h" #include "Map.h"
#include "Music.h" #include "Music.h"
#include "Unreachable.h" #include "Unreachable.h"
#include "UtilityClass.h" #include "UtilityClass.h"
#include "VFormat.h"
#include "Vlogging.h" #include "Vlogging.h"
#include "Xoshiro.h" #include "Xoshiro.h"
@ -36,6 +40,11 @@ scriptclass::scriptclass(void)
textx = 0; textx = 0;
texty = 0; texty = 0;
textflipme = false; textflipme = false;
textcentertext = false;
textpad_left = 0;
textpad_right = 0;
textpadtowidth = 0;
textcase = 1;
} }
void scriptclass::clearcustom(void) void scriptclass::clearcustom(void)
@ -504,6 +513,13 @@ void scriptclass::run(void)
txt.push_back(commands[position]); txt.push_back(commands[position]);
} }
} }
textcentertext = false;
textpad_left = 0;
textpad_right = 0;
textpadtowidth = 0;
translate_dialogue();
} }
else if (words[0] == "position") 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. //the textbox cannot be outside the screen. Fix if it is.
if (textx <= -1000) if (textx <= -1000)
{ {
@ -1840,6 +1870,7 @@ void scriptclass::run(void)
} }
else if (words[0] == "specialline") else if (words[0] == "specialline")
{ {
//Localization is handled with regular cutscene dialogue
switch(ss_toi(words[1])) switch(ss_toi(words[1]))
{ {
case 1: case 1:
@ -1862,6 +1893,8 @@ void scriptclass::run(void)
} }
break; break;
} }
translate_dialogue();
} }
else if (words[0] == "trinketbluecontrol") 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++; 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) static void gotoerrorloadinglevel(void)
{ {
game.createmenu(Menu::errorloadinglevel); game.createmenu(Menu::errorloadinglevel);
@ -2474,9 +2590,13 @@ void scriptclass::startgamemode(const enum StartMode mode)
game.nocutscenes = true; game.nocutscenes = true;
game.intimetrial = true; game.intimetrial = true;
game.timetrialcountdown = 150; game.timetrialcountdown = 150;
game.timetrialparlost = false;
game.timetriallevel = mode - Start_FIRST_TIMETRIAL; game.timetriallevel = mode - Start_FIRST_TIMETRIAL;
if (map.invincibility)
{
game.sabotage_time_trial();
}
switch (mode) switch (mode)
{ {
case Start_TIMETRIAL_SPACESTATION1: case Start_TIMETRIAL_SPACESTATION1:
@ -2912,6 +3032,7 @@ void scriptclass::hardreset(void)
game.timetrialshinytarget = 0; game.timetrialshinytarget = 0;
game.timetrialparlost = false; game.timetrialparlost = false;
game.timetrialpar = 0; game.timetrialpar = 0;
game.timetrialcheater = false;
game.totalflips = 0; game.totalflips = 0;
game.hardestroom = "Welcome Aboard"; game.hardestroom = "Welcome Aboard";
@ -3241,6 +3362,15 @@ void scriptclass::loadcustom(const std::string& t)
}else if(words[0] == "iftrinketsless"){ }else if(words[0] == "iftrinketsless"){
if(customtextmode==1){ add("endtext"); customtextmode=0;} if(customtextmode==1){ add("endtext"); customtextmode=0;}
add("custom"+lines[i]); 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"){ }else if(words[0] == "destroy"){
if(customtextmode==1){ add("endtext"); customtextmode=0;} if(customtextmode==1){ add("endtext"); customtextmode=0;}
add(lines[i]); add(lines[i]);

View File

@ -77,6 +77,8 @@ public:
void run(void); void run(void);
void translate_dialogue(void);
void startgamemode(enum StartMode mode); void startgamemode(enum StartMode mode);
void teleport(void); void teleport(void);
@ -99,6 +101,11 @@ public:
int texty; int texty;
int r,g,b; int r,g,b;
bool textflipme; bool textflipme;
bool textcentertext;
size_t textpad_left;
size_t textpad_right;
size_t textpadtowidth;
char textcase;
//Misc //Misc
int i, j, k; int i, j, k;

View File

@ -7,6 +7,7 @@ void scriptclass::load(const std::string& name)
//loads script name t into the array //loads script name t into the array
position = 0; position = 0;
commands.clear(); commands.clear();
scriptname = name;
running = true; running = true;
const char* t = name.c_str(); const char* t = name.c_str();

View File

@ -1,5 +1,6 @@
#include "Textbox.h" #include "Textbox.h"
#include <SDL.h>
#include <utf8/unchecked.h> #include <utf8/unchecked.h>
textboxclass::textboxclass(void) textboxclass::textboxclass(void)
@ -103,8 +104,9 @@ void textboxclass::resize(void)
if (len > (unsigned int)max) max = len; if (len > (unsigned int)max) max = len;
} }
w = (max +2) * 8; // 16 for the borders
h = (lines.size() + 2) * 8; w = max*8 + 16;
h = lines.size()*8 + 16;
} }
void textboxclass::addline(const std::string& t) void textboxclass::addline(const std::string& t)
@ -113,3 +115,40 @@ void textboxclass::addline(const std::string& t)
resize(); resize();
if ((int) lines.size() >= 12) lines.clear(); 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);
}

View File

@ -26,6 +26,12 @@ public:
void resize(void); void resize(void);
void addline(const std::string& t); 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: public:
//Fundamentals //Fundamentals
std::vector<std::string> lines; std::vector<std::string> lines;

View File

@ -27,3 +27,31 @@ tinyxml2::XMLDeclaration* update_declaration(tinyxml2::XMLDocument& doc);
tinyxml2::XMLComment* update_comment(tinyxml2::XMLNode* parent, const char* text); tinyxml2::XMLComment* update_comment(tinyxml2::XMLNode* parent, const char* text);
} // namespace xml } // 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)

View File

@ -16,6 +16,8 @@
#include "Input.h" #include "Input.h"
#include "InterimVersion.h" #include "InterimVersion.h"
#include "KeyPoll.h" #include "KeyPoll.h"
#include "Localization.h"
#include "LocalizationStorage.h"
#include "Logic.h" #include "Logic.h"
#include "Map.h" #include "Map.h"
#include "Music.h" #include "Music.h"
@ -368,6 +370,8 @@ int main(int argc, char *argv[])
{ {
char* baseDir = NULL; char* baseDir = NULL;
char* assetsPath = NULL; char* assetsPath = NULL;
char* langDir = NULL;
char* fontsDir = NULL;
bool seed_use_sdl_getticks = false; bool seed_use_sdl_getticks = false;
#ifdef _WIN32 #ifdef _WIN32
bool open_console = false; bool open_console = false;
@ -420,6 +424,20 @@ int main(int argc, char *argv[])
assetsPath = argv[i]; 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")) else if (ARG("-playing") || ARG("-p"))
{ {
ARG_INNER({ ARG_INNER({
@ -482,6 +500,10 @@ int main(int argc, char *argv[])
{ {
vlog_toggle_error(0); vlog_toggle_error(0);
} }
else if (ARG("-translator"))
{
loc::show_translator_menu = true;
}
#ifdef _WIN32 #ifdef _WIN32
else if (ARG("-console")) 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 #ifdef _WIN32
if (open_console) if (open_console)
{ {
@ -508,7 +534,7 @@ int main(int argc, char *argv[])
} }
#endif #endif
if(!FILESYSTEM_init(argv[0], baseDir, assetsPath)) if(!FILESYSTEM_init(argv[0], baseDir, assetsPath, langDir, fontsDir))
{ {
vlog_error("Unable to initialize filesystem!"); vlog_error("Unable to initialize filesystem!");
VVV_exit(1); VVV_exit(1);
@ -608,12 +634,24 @@ int main(int argc, char *argv[])
gameScreen.init(&screen_settings); gameScreen.init(&screen_settings);
} }
loc::loadtext(false);
loc::loadlanguagelist();
game.createmenu(Menu::mainmenu);
graphics.create_buffers(gameScreen.GetFormat()); graphics.create_buffers(gameScreen.GetFormat());
if (game.skipfakeload) if (game.skipfakeload)
game.gamestate = TITLEMODE; game.gamestate = TITLEMODE;
if (game.slowdown == 0) game.slowdown = 30; 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 //Check to see if you've already unlocked some achievements here from before the update
if (game.swnbestrank > 0){ if (game.swnbestrank > 0){
if(game.swnbestrank >= 1) game.unlockAchievement("vvvvvvsupgrav5"); if(game.swnbestrank >= 1) game.unlockAchievement("vvvvvvsupgrav5");
@ -757,6 +795,7 @@ static void cleanup(void)
music.destroy(); music.destroy();
map.destroy(); map.destroy();
NETWORK_shutdown(); NETWORK_shutdown();
loc::resettext(true);
SDL_Quit(); SDL_Quit();
FILESYSTEM_deinit(); FILESYSTEM_deinit();
} }