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/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

View File

@ -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;

View File

@ -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

View File

@ -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)
{

View File

@ -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<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)
{
#ifdef _WIN32

View File

@ -5,16 +5,23 @@
class binaryBlob;
#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
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<std::string> FILESYSTEM_getLanguageCodes(void);
bool FILESYSTEM_levelDirHasError(void);
void FILESYSTEM_clearLevelDirError(void);
const char* FILESYSTEM_getLevelDirError(void);

View File

@ -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);

View File

@ -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<MenuOption> 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;

View File

@ -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<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*/ ) {
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);
}
}

View File

@ -5,6 +5,7 @@
#include <string>
#include <vector>
#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 */
};

View File

@ -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;

View File

@ -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;
}

View File

@ -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;

View File

@ -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); \

View File

@ -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);

View File

@ -4,6 +4,7 @@
#include <limits.h>
#include <SDL_timer.h>
#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]);

View File

@ -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;

View File

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

View File

@ -1,5 +1,6 @@
#include "Textbox.h"
#include <SDL.h>
#include <utf8/unchecked.h>
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);
}

View File

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