Page MenuHomePhabricator (Chris)

No OneTemporary

Authored By
Unknown
Size
718 KB
Referenced Files
None
Subscribers
None
This file is larger than 256 KB, so syntax highlighting was skipped.
diff --git a/AUTHORS b/AUTHORS
index 2747e95..489a8b1 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -1,56 +1,59 @@
Active developers
acme_pjz
Edward Lii
MCMic
odamite
oyvindln (SDL2 port)
Tedium (Cloudscape theme)
Former developers
Luka Horvat
O. Bahri Gordebak
Contributors
AapoRantalainen
ctdabomb (Testing, levelmaking)
davy
emarshall85
+ GunChleoc
hasufell
Sauer2
squarecross
TermiT
worldcitizen
+ Wuzzy
Yann Soubeyrand
Ports/Packaging
AapoRantalainen - Maemo port
acme_pjz - Windows version
amdmi3 - FreeBSD port
Artur_J - AmigaOS port
Edward Lii - linux binary, openSUSE packaging
hasufell - gentoo packaging
hcf - Xbox port
kirpken - Web port
Knitter - MacOS X port
mcobit - Pandora port
MCMic - Arch Linux packaging
odamite - Ubuntu packaging, Windows installer
petos - Mageia packaging
Translators
acme_pjz - Simplified Chinese
Akien, Poussinou - French
BioHazardX - Italian
+ eugeneloza - Ukrainian
GunChleoc - Scottish Gaelic
- KroArtem, mesnevi - Russian
+ KroArtem, mesnevi, eugeneloza - Russian
mdtrooper - Spanish
ming.yan2 - Traditional Chinese
odamite - Finnish
Petter Reinholdtsen, comradekingu - Norwegian Bokmål
SanskritFritz - Hungarian
Tedium - Dutch
Wuzzy (with help of Sauer2) - German
For an up to date list and contact information see:
http://meandmyshadow.sourceforge.net/wiki/index.php/Authors
diff --git a/CMakeLists.txt b/CMakeLists.txt
index e4fbdf8..6321d9d 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,182 +1,185 @@
Project (meandmyshadow)
CMake_Minimum_Required (VERSION 3.1)
Set (CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/Modules/")
+#Path options
+Set (BINDIR "bin" CACHE STRING "Where to install binaries")
+Set (DATAROOTDIR "${CMAKE_INSTALL_PREFIX}/share" CACHE STRING "Sets the root of data directories to a non-default location")
+Set (ICONDIR "${DATAROOTDIR}/icons" CACHE STRING "Sets the icon directory for desktop entry to a non-default location.")
+Set (DESKTOPDIR "${DATAROOTDIR}/applications" CACHE STRING "Sets the desktop file directory for desktop entry to a non-default location.")
+
+#Options
Option (DEBUG_MODE "Compile the game with debug mode enabled" OFF)
Option (DISABLED_DEBUG_STUFF "Enable this you'll see a lot of annoying script debug messages which will lag the game." OFF)
#Find the required libraries.
Find_Package (SDL2 REQUIRED)
Find_Package (SDL2_image REQUIRED)
Find_Package (SDL2_ttf REQUIRED)
Find_Package (SDL2_mixer REQUIRED)
Find_Package (CURL REQUIRED)
Find_Package (LibArchive REQUIRED)
Find_Package (Lua REQUIRED)
if (NOT SDL2_FOUND)
message (FATAL_ERROR "SDL2 library could not be found!")
endif (NOT SDL2_FOUND)
if (NOT SDL2_IMAGE_FOUND)
message (FATAL_ERROR "SDL2_image library could not be found!")
endif (NOT SDL2_IMAGE_FOUND)
if (NOT SDL2_TTF_FOUND)
message (FATAL_ERROR "SDL2_ttf library could not be found!")
endif (NOT SDL2_TTF_FOUND)
if (NOT SDL2_MIXER_FOUND)
message (FATAL_ERROR "SDL2_mixer library could not be found!")
endif (NOT SDL2_MIXER_FOUND)
if (NOT CURL_FOUND)
message(FATAL_ERROR "CURL library could not be found!")
endif (NOT CURL_FOUND)
if (NOT LibArchive_FOUND)
message (FATAL_ERROR "LibArchive library could not be found!")
endif (NOT LibArchive_FOUND)
if (NOT LUA_FOUND)
message (FATAL_ERROR "Lua library could not be found!")
endif (NOT LUA_FOUND)
if (LUA_VERSION_STRING VERSION_LESS "5.2")
message (FATAL_ERROR "Lua version too old ${LUA_VERSION_STRING}, expected at least 5.2!")
endif ()
# check version from Globals.h
file(READ "${PROJECT_SOURCE_DIR}/src/Globals.h" GLOBALS_H)
string(REGEX MATCH "version[ ]*=[ ]*\"[^\"]*\"" MNMS_VERSION_STR ${GLOBALS_H})
string(REGEX REPLACE "^[^\"]*\"([^\"]*)\".*$" "\\1" MNMS_VERSION_STR ${MNMS_VERSION_STR})
message(STATUS "The version read from Globals.h is: ${MNMS_VERSION_STR}")
string(REGEX REPLACE "^V([0-9.]+).*$" "\\1" MNMS_VERSION_NUM ${MNMS_VERSION_STR})
set(MNMS_VERSION_NUM "${MNMS_VERSION_NUM}.0.0.0.0")
string(REGEX REPLACE "^([0-9]+)[.]([0-9]+)[.]([0-9]+)[.]([0-9]+).*$" "\\1,\\2,\\3,\\4" MNMS_VERSION_NUM ${MNMS_VERSION_NUM})
message(STATUS "which is: ${MNMS_VERSION_NUM}")
# check version from git
find_package(Git)
if(GIT_FOUND)
exec_program(${GIT_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR} ARGS "describe"
OUTPUT_VARIABLE MNMS_GIT_VERSION RETURN_VALUE GIT_RETURN_VALUE)
if(GIT_RETURN_VALUE STREQUAL "0")
set(MNMS_VERSION_STR "${MNMS_VERSION_STR} (${MNMS_GIT_VERSION})")
message(STATUS "The version read from git is: ${MNMS_GIT_VERSION}")
else()
# possibly there are no any tags
exec_program(${GIT_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR} ARGS "rev-parse --short HEAD"
OUTPUT_VARIABLE MNMS_GIT_VERSION RETURN_VALUE GIT_RETURN_VALUE)
if(GIT_RETURN_VALUE STREQUAL "0")
set(MNMS_VERSION_STR "${MNMS_VERSION_STR} (git ${MNMS_GIT_VERSION})")
message(STATUS "The version read from git is: ${MNMS_GIT_VERSION}")
endif()
endif()
endif()
# show version information on Windows
Set(WIN32_RESOURCES )
if(WIN32)
Configure_File (
"${PROJECT_SOURCE_DIR}/icons/windows-icon/res.rc.in"
"${PROJECT_BINARY_DIR}/res.rc"
)
Set(WIN32_RESOURCES ${PROJECT_BINARY_DIR}/res.rc)
Include_Directories(${PROJECT_SOURCE_DIR}/icons/windows-icon/)
SOURCE_GROUP("Source Files\\Resources" FILES ${WIN32_RESOURCES})
endif()
#Parse the configure file.
Configure_File (
"${PROJECT_SOURCE_DIR}/src/config.h.in"
"${PROJECT_BINARY_DIR}/config.h"
)
#Add some missing libraries to Windows.
if(WIN32)
include_directories(${PROJECT_SOURCE_DIR}/src/libs/dirent)
endif(WIN32)
#Disable some annoying warnings.
if(MSVC)
# warning C4996: '***': This function or variable may be unsafe
add_definitions(/wd4996)
else()
# Assume it's gcc or clang
# warning: '***' overrides a member function but is not marked 'override'
add_definitions(-Wno-inconsistent-missing-override)
endif()
#Define some debug stuff.
if(DEBUG_MODE)
add_definitions(-DDEBUG)
add_definitions(-D_DEBUG)
endif(DEBUG_MODE)
if(DISABLED_DEBUG_STUFF)
add_definitions(-DDISABLED_DEBUG_STUFF)
endif(DISABLED_DEBUG_STUFF)
#Add the include directories of the (found) libraries.
Include_Directories(
${PROJECT_BINARY_DIR}
${SDL2_INCLUDE_DIR}
${SDL2_IMAGE_INCLUDE_DIR}
${SDL2_TTF_INCLUDE_DIR}
${SDL2_MIXER_INCLUDE_DIR}
${CURL_INCLUDE_DIR}
${LibArchive_INCLUDE_DIR}
${LUA_INCLUDE_DIR}
${PROJECT_SOURCE_DIR}/src/libs
)
#Set the output path and the source path.
Set (EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR})
Set (SRC_DIR ${PROJECT_SOURCE_DIR}/src)
#List the source files.
File (GLOB SOURCES ${SRC_DIR}/*.cpp)
+File (GLOB TINYFORMAT ${SRC_DIR}/libs/tinyformat/*.cpp)
File (GLOB TINYGETTEXT ${SRC_DIR}/libs/tinygettext/*.cpp)
File (GLOB FINDLOCALE ${SRC_DIR}/libs/findlocale/*.cpp)
#Always use SDL_iconv in tinygettext
add_definitions(-DHAVE_SDL)
+SOURCE_GROUP("Source Files\\tinyformat" FILES ${TINYFORMAT})
SOURCE_GROUP("Source Files\\tinygettext" FILES ${TINYGETTEXT})
SOURCE_GROUP("Source Files\\findlocale" FILES ${FINDLOCALE})
-Add_Executable (meandmyshadow ${SOURCES} ${TINYGETTEXT} ${FINDLOCALE} ${WIN32_RESOURCES})
+Add_Executable (meandmyshadow ${SOURCES} ${TINYFORMAT} ${TINYGETTEXT} ${FINDLOCALE} ${WIN32_RESOURCES})
set_property(TARGET meandmyshadow PROPERTY CXX_STANDARD 11)
Target_Link_Libraries (
meandmyshadow
${SDL2_LIBRARY}
${SDL2_IMAGE_LIBRARY}
${SDL2_TTF_LIBRARY}
${SDL2_MIXER_LIBRARY}
${SDL2MAIN_LIBRARY}
${CURL_LIBRARY}
${LibArchive_LIBRARY}
${LUA_LIBRARIES}
)
-#Path options
-Set (BINDIR "bin" CACHE STRING "Where to install binaries")
-Set (DATAROOTDIR "${CMAKE_INSTALL_PREFIX}/share" CACHE STRING "Sets the root of data directories to a non-default location")
-Set (ICONDIR "${DATAROOTDIR}/icons" CACHE STRING "Sets the icon directory for desktop entry to a non-default location.")
-Set (DESKTOPDIR "${DATAROOTDIR}/applications" CACHE STRING "Sets the desktop file directory for desktop entry to a non-default location.")
-
#Install locations
Install (DIRECTORY ${PROJECT_SOURCE_DIR}/data DESTINATION ${DATAROOTDIR}/meandmyshadow/)
Install (FILES AUTHORS DESTINATION ${DATAROOTDIR}/meandmyshadow/)
Install (TARGETS meandmyshadow RUNTIME DESTINATION ${BINDIR})
if ("${CMAKE_SYSTEM_NAME}" MATCHES "Linux")
Install (FILES meandmyshadow.desktop DESTINATION ${DESKTOPDIR})
Install (FILES icons/16x16/meandmyshadow.png DESTINATION ${ICONDIR}/hicolor/16x16/apps/)
Install (FILES icons/32x32/meandmyshadow.png DESTINATION ${ICONDIR}/hicolor/32x32/apps/)
Install (FILES icons/48x48/meandmyshadow.png DESTINATION ${ICONDIR}/hicolor/48x48/apps/)
Install (FILES icons/64x64/meandmyshadow.png DESTINATION ${ICONDIR}/hicolor/64x64/apps/)
endif ("${CMAKE_SYSTEM_NAME}" MATCHES "Linux")
diff --git a/ChangeLog b/ChangeLog
index 9a0290d..b3598f4 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,326 +1,339 @@
-Me and My Shadow V0.5 (RC)
+Me and My Shadow V0.5
------------------------
+
+Brief list of changes:
+
* Switch to SDL2.
* Menu theme.
* Achievement and statistics system.
* Scripting with lua.
* Pushable block.
* Improved addon dialog.
* Improved level editor user interface.
* Sizable blocks.
* Scenery blocks.
* Undo/redo in level editor originally written by squarecross.
* Added 'visible' property to blocks.
+* Custom appearance of blocks.
+* Various other bug fixes and minor improvements.
Translations:
-* Updated Simplified Chinese, German, and Scottish Gaelic translations.
+* Updated Simplified Chinese, German, Russian, French and Scottish Gaelic translations.
* Added Hungarian translation by SanskritFritz.
+* Added Norwegian translation by Petter Reinholdtsen and Allan Nordhøy.
+* Added Ukrainian translation by eugeneloza.
Known bugs and limitations:
* No proper IME support for text box, etc.
* OSX support is broken.
+* The button specification in theme file is changed, old theme may render button incorrectly in new version.
+* Some achievements are not realistic, and there is a typo in achievements.
+* Invisible collectible is counted in total number of collectibles.
+* The scenery layer naming convention is confusing.
+* The target time for tutorial level 10 seems unbeatable. Needs further investigation.
+* There seems to be a random crash bug when exiting the game.
Me and My Shadow V0.4
------------------------
* Fixed the .desktop file.
* Created a separate docs/ folder to contain the documentation files like Controls.txt, ThemeDescription.txt, etc...
* Implemented an OpenGL renderer as alternative for the SDLRenderer.
* Fixed a bug where escape in the options menu would exit both the options menu and the main menu.
* Updated all GUIs and menus to support different resolutions, making them dynamic.
* Extended the Game rendering to support different resolutions.
* Extended the CMakeLists.txt file to make paths configurable.
* Added the library tinygettext to the project to support localisation.
* Added the library tinyformat to allow easy string formatting.
* Made the internal string literals translatable.
* Added the library findlocale to detect the preferred localization.
* Updated the CMakeLists.txt to compile the bundled libraries.
* Added the font Droid Sans for languages that contain non-latin characters.
* Implemented a levelpack manager for preloading the levelpacks at the start.
* Made levelpacks translatable as well.
* Fixed a bug where the game would crash on translating the tooltip for levels of the 'Custom Levels' levelpack.
* Changed the number of levels per row in the LevelSelect screens to support multiple resolutions.
* Fixed a bug where levelpack translations weren't detected properly.
* Extended the LevelEditor to support different window sizes.
* Added language and resolution options in the Options menu.
* Made 800x600 the minimum resolution supported.
* Fixed a bug where the camera would move in the leveleditor even if the mouse was on top of the toolbar.
* Added support for different die animations, one for dying while looking right and one for looking left.
* Also changed the way the level is aligned when the screen is larger than the level, the bottom of the level will stay the bottom of the window/screen.
* Fixed a bug where the camera didn't scroll smooth to the left.
* Updated the Cloudscape theme to V2.1.
* Fixed some in-game tooltip memory leak.
* Warnings thrown by tinygettext are now suppressed.
* Fixed a bug in the the screen surface where the alpha mask wasn't configured properly when using the gl renderer.
* Added a python script for generating a .pot file by extracting the translatable strings out of a levelpack.
* Updated the time and recordings icons, they are now black and white.
* Fixed a bug where after restarting the inter level gui would appear.
* Fixed a bug where the player or the shadow got displaced when being squashed.
* Levels get centred in the leveleditor when smaller than the screen size.
* Updated the Cloudscape theme to V2.2.
* Fixed some bugs regarding the new levelpackmanager and installing/removing addons.
* Also applied a fix for TreeStorageNode.cpp to prevent some compile errors.
* Disabled the death animation when falling of the level.
* Fixed a bug where the player or the shadow would die when jumping on a block that has a spike behind it.
* The block configuration screens in the leveleditor are now centred.
* Added toolbox for easy selecting block types in the leveleditor.
* Level selection screen can now be controlled with only the keyboard or a gamepad.
* Added command line arguments for configuring resolution and window or fullscreen mode.
* Added collectables to the game.
* Made the sound and music options in the options menu a value instead of an on/off toggle.
* Added a new GUIObject for selecting a value inside a given range, GUISlider.
* Changing the sound or music in the options menu is now applied directly.
* Added command line options for configuring sound and music volume.
* Changed the addons menu to use a GUISingleLineListBox instead of three separate GUI?ObjectButtons.
* Fixed a bug where the music volume wasn't updated while adjusting it.
* Added caching support to GUIObjects, text is only rendered when changed or needed.
* Changing resolution or language doesn't require a restart any more.
* Fixed a bug where the chaching didn't update in GUISingleLineListBox.
* Fixed a bug where the player or shadow became immortal when standing on top of a moving block that moved through spikes.
* Added gravity and automatic width to the GUIObjectButton.
* Added resolution enumeration using SDL_ListModes(), filtering out resolutions smaller than the minimum (800x600).
* Fixed some bugs regarding the gravity parameter in the GUIObject which broke the GUISlider and GUIObjectCheckbox.
* Fixed some memory leaks when changing resolution.
* Added scaling support for themes to rescale instead of reloading the whole theme.
* The game window is now sizable.
* Fixed a bug in the leveleditor where the pressed mouse button was checked using event.type while not in handleEvent() but in logic().
* Removed some old copy code that could cause deletion of all levels in a levelpack.
* Fixed a memory bug when using openGL mode and resizing the window.
* Fixed the font size of single line list box and a memory leak in GUIObject.
* Fixed a memory leak in the MusicManager.
* Fixed a compile warning in the Main.cpp file regarding a translatable string.
* Updated the tutorial levelpack to include the new collectable.
* Fixed another memory leak in the MusicManager.
* Implemented a proper method for limiting the resizing of the window below 800x600 for Linux (X11) systems.
* Implemented a method for rearranging GUI elements upon resizing the window.
* Added a shell script to add the key names to the .pot file.
* Fixed a memory leak in font loading and window resizing.
* Added Compiling.txt file containing compiling instructions for Linux systems.
* Buttons in the options menu use a smaller font when there's not enough space.
* Fixed the constant invocation of onVideoResize() bug.
* Added a minimum window size limit (800x600) for Windows systems.
* Fixed a bug where the currentID would be incorrect after postLoad when there was a teleporter in the level which wasn't the last in the levelObjects vector with an id.
* Applied patch by worldcitizen, which fixes some compile issues when using gcc 4.7.
* Fixed a bug that Windows doesn't have stdint.h but source file tried to included it.
* Fixed a bug where key names weren't translated.
* Changed the notification block's message dialog size.
* Fixed Cloudscape as default theme.
* Fixed some issues with the rendering of the movingspeed text in the [[LevelEditor}leveleditor]].
* Cleaned up the Main.cpp, moving some initialisation stuff in the appropriate init method.
* Fixed an issue regarding arbitrary fullscreen resolutions.
* Made the help message for the command line untranslatable since the dictionary manager isn't and can't be loaded before showing it.
* Added a shortcut for toggling fullscreen (Alt+Enter).
* Fixed a bug where the user could restart the level while playing a recording.
* Cursor is now invisible during game-play, both in the game state and the play mode of the leveleditor.
* Changed the draw order of the player and the shadow, the player is now drawn last.
* Fixed translated time and recordings labels in the level select screen.
* Fixed a clipping issue with the knewave font.
* Added more music by Juho-Petteri Yliuntinen.
* Left clicking objects in the leveleditor with the configure tool will now show the properties dialog of that block, if any.
* Fixed some bugs with levelpack translations.
* Camera changes focus back to the player when the shadow dies.
* Fixed some issues with long strings in the level editor.
* Fixed some issues regarding resizing and GUIGravityCenter with GUILabels.
* Fixed a bug where the game stopped responding or gave a black screen when resizing the window with a dialog on top.
* Updated the Cloudscape theme with the new collectable made by Tedium.
* Added a sound for picking up collectables.
* Exit now has an open and a closed state.
* Updated collectable GUI to match Tedium's mockup
* Implemented GUIOverlays to solve the black background when resizing the window with a dialog on top.
* Fixed a bug with resizing in the leveleditor where the placement surface wasn't recreated.
* Fixed the enterLoop method of the GUIOverlay to also call the resize method of the parentState.
* Fixed the name convention of the GUIObjectRoot when using a GUIOverlay to improve readability of the code.
* The number of collectables collected in the HUD is now hidden in the leveleditor.
* Message boxes can now be closed by pressing escape, return or backspace.
* Fixed a bug where the player could shift in front of a moving block instead of getting squashed when standing on top of his shadow.
* Made the error messages in the Addons menu translatable.
* Fixed a bug where the configure dialog of switches and buttons didn't show the configured behaviour when using any language other than English.
* Fixed the .desktop file by removing a duplicate category, thanks to hasufell for pointing this out.
* Only the fonts are reloaded now when changing the language instead of reloading everything.
* Fixed the copyright notice at the top of each source file.
* Added the Credits file for the classic theme.
* Added an AUTHORS file, basically a copy of the wiki page Authors.
* Replaced the hit.wav and jump.wav files with sounds we know are free, made by odamite under CC0.
* Updated the credits file, there's now one central Credits.txt that contains all the licenses of the art used in meandmyshadow or pointers to that information.
* Added a license header to the source files in the tools folder.
* Removed the misc folder with the Empty.map.
* A new (empty) map is now created internally instead of loaded from an empty file.
* Changed the Name value in the .desktop file to match the name of the game with correct capitalisation.
* Changed the location the addons file is fetched from, the addons git repository instead of the project web.
* Implemented a Credits screen.
* Fixed bug where picked up collectables didn't save.
* The Credits menu is now filled with text from the files AUTHORS and Credits.txt.
* Added music credits to the credits screen.
* Fixed the Name field of the music files, there were no quites around the name which contained a space.
* Added horizontal scrollbar in the credits screen
* Fixed the graphics on a horizontal GUIScrollBar.
* Removed the Credits menu entry and added an icon to the lower right corner.
* Tried to fix a segfault in the LevelSelect screens when navigating with the keyboard.
* Added the translatable string credits and updated the translations by looking up the translation from other open source projects.
* Corrected the translation files' headers.
* Updated the credits icon.
* Updated the headers of the levelpack translations.
* Added a ChangeLog file.
Translations added:
* Russian translations for the game, default, tutorial and classic levelpack by KroArtem.
* Italian translations for the game, default, tutorial and classic levelpack by BioHazardX.
* Finnish translations for the game, default, tutorial and classic levelpack by odamite.
* Simplified Chinese for the game, default, tutorial and classic levelpack by acme_pjz.
* Traditional Chines for the game by ming.yan2.
* German translations for the game and tutorial levelpack by Wuzzy.
* Dutch translation for the game by Tedium.
Me and My Shadow V0.3
------------------------
* An input manager was added to allow the configuration of key bindings.
* The format of the progress files of levelpacks was changed to the POA format.
* Added a teleport option in the level editor to make testing easier, default key binding is F5.
* Fixed a bug where non-latin file and path names caused unexpected behaviour under Windows.
* The menu and GUI theme was changed to fit with the new default theme Cloudscape matching the mock-ups by odamite. (link)
* The name of the 14th level of the classic levelpack changed from 'Damn' to 'Headache'.
* Extension were automatically added to file names in save dialogs if not present.
* Separate levels can be played again through a special levelpack named Levels
* The shortcut Ctrl+s was removed which toggled the sound and music on and off.
* Save dialogs can now start with an empty filename field.
* Two level statistics where added: time and recordings.
* Medals can be earned by beating a set target time and recordings.
* Level names of levels inside a levelpack are now retrieved from the level file itself instead of the levelpack file.
* The game now recorded user input to be able to replay it later on.
* A carrot was added to the GUITextBox to allow easier editing.
* Joystick support has been added to the InputManager.
* MD5sum are used to link replays and statistics to levels.
* The CMakeLists.txt was updated to include openssl and crypto.
* Fixed a bug where the level statistics were always updated, now the best stays.
* The best time and best number of recordings replays are auto-saved.
* Level selection blocks updated to match the Cloudscape theme.
* Added animation for the arrows of the GUISingleLineListBox.
* All key bindings can now have a primary and alternative key.
* GUITextBox and GUITextArea now handle delete and backspace properly.
* A section at the bottom of the LevelSelect screen was added to show level and level statistic information.
* A method was made for drawing so named GUIBoxes.
* Target time and recordings can be configured in the leveleditor.
* Separate the up and jump key and the down and action keys in the input manager.
* Removed the non-free music that was there from the initial release.
* Replays are shown after completing a level.
* The help menu got removed and the entry in the main menu was replaced with the addons menu.
* The clear progress option was moved to the options menu.
* Added a music manager to add support for multiple music tracks.
* Sound and music is now separated in the options menu.
* A message box shows instead of a label to when a certain changes requires the user to restart the game.
* A bug was fixed in the md5 calculation of the TreeStorageNode.
* Changed the drawGUIBox to not use rounded rectangles to support older versions of SDL_gfx.
* Fixed a bug where an extension was added to a file dialog that was used for folders.
* Fixed a bug regarding the player holding the shadow whilst on a moving block.
* Added a LevelEditSelect screen to replace the old levelpackeditor.
* Target times and recordings added for the classic levelpack thanks to Tedium.
* Fixed a bug where the player could continue recording after he died.
* Menu theme music added, made by vaev (Juho-Petteri Yliuntinen).
* Extended the MusicManager to support a separate loop file as alternative to a loop start time.
* Added icons to the tooltips in the LevelSelect screen.
* Changing block type in the level editor is now a separate key binding.
* Added an interlevel popup to show the target time and recordings also shows the achieved medal.
* Made Me and My Shadow as XDG-compliant as possible by saving user data in ~/.local/share/ and config files in ~/.config/.
* Notifications aren't shown when the inter level popup is up.
* Fixed a bug where an empty levelpack could crash the game.
* Fixed the bug where the replay button of last level doesn't show up.
* Changed the way notification blocks are displayed, there's no popup any more, but a GUIBox at the bottom of the screen.
* Fixed a bug regarding the leveleditor crashing because of dangling pointer objNotificationBlock.
* Updated the Cloudscape theme made by Tedium.
* Changed colour of the '&'-sign in logo to match the updated background image.
* Fixed selection overlapping in the GUIListBox.
* Both the player and shadow can now get squashed when between a solid block and a moving block.
* Updated the Controls.txt to include all the (new) keybindings.
* Made it possible to configure the starting state of fragile blocks.
* Added check to prevent overwriting levels using the LevelEditSelect screen.
* Added notification for when the shadow dies.
* Updated the icons to match the latest Cloudscape version.
Me and My Shadow V0.2
------------------------
* The GUIScrollBar was added and used in the LevelSelect screen when there are too many levels to fit on the screen.
* The GUIListBox control was added and used in filedialogs.
* Support for levelpacks was added.
* An option was added to the LevelSelect screen to play custom levels made in the editor.
* Added a levelpack editor to the leveleditor.
* Added an options menu, with this came the settings file.
* A theme manager was added to support theming.
* The screen gets dimmed when a GUI is opened.
* Some improvements to the POAParser.
* The help menu graphics where updated by removing the dots in the background.
* Added an addon manager to allow downloadable themes, levelpacks and levels
* The data folder was structured more by separating levels from levelpacks and by adding a separate folder for themes.
* Libarchive became a dependency of Me and My Shadow because it is used in the Addons menu.
* The code underwent massive refactoring and documenting to match a set of code conventions.
* Bug fixed where the player or shadow could teleport by restarting a level whilst touching a moving block.
* The notification block was added to the game.
* Fixed a bug where the player could jump on a fragile block that was destroyed if the jump was timed correctly.
* An internet proxy option was added.
* The |leveleditor got a massive overhaul, basically being built up from scratch.
* Primitive drawing methods where added to the Functions.cpp file.
* Focus support was added to the GUIObjectTextBox.
* A [[GUITextArea] was added to support multiline text input.
* User created content was separated from main and addon content.
* A congratulation text was added when finishing a levelpack.
* Fixed a swap bug, causing the shadow the sink in the floor.
* The tutorial levelpack was added.
* The original levelpack was renamed to classic.
* The drawing of primitives was handed over to SDL_gfx
* A help screen system was introduced that consisted out of multiple slides explaining the certain aspects of the game.
* An icon indicating the recording status was added to the upper left corner of the screen.
* The tab key allows the player to switch the camera focus between the player and the shadow.
* Both the player and the shadow can be themed using the same system as blocks.
* Resetting doesn't reset the saved state any more, allowing the player to reload a checkpoint after resetting (by accident).
* A new default levelpack was added containing levels ranging from easy to medium difficulty.
* Jump and fall animation support was added.
* The Theme:Cloudscape became the default theme, the default theme is renamed to classic just like the levelpack.
* Tooltips where added to the toolbar in the leveleditor.
* An icon for the Windows build was added.
* CMake modules where bundled to make packaging easier for systems missing these modules.
* The order in which the blocks appear in the leveleditor have been changed, instead of using the order they appear internally.
* Some compiler warnings fixed, adding newlines to the end of each source file for example.
* Fixed a problem with older version of libarchive.
* Added a separate update button to the addons menu so that a user can uninstall an addon without being forced to update it first.
* Fixed a bug where the program wouldn't quit when in the leveleditor.
* The up and down arrow keys can now be used to navigate through the main menu.
Me and My Shadow V0.1.2
------------------------
* The POAParser was updated to handle non-existing files better.
* Some fixes were made to prevent a crash under Linux.
* Command line arguments added to change config and data paths.
* Automatic data path detection was added.
* Missing background music changed from an error to a warning.
* .desktop file and icon added.
* Tooltip added to levelblocks in the LevelSelect screen to show the level's name.
Me and My Shadow V0.1.1
------------------------
* The TitleMenu has been removed.
* The transition between states has been changed to a fade transition.
* The code was refactored (source files in src/ folder and header files per class)
* The background of both the menu and the game have been changed to remove the spots.
* The movement system has changed, in V0.1 the player would always record.
* Data files rearranged.
* Improved the blocks graphics using the Gem Jewel Diamond Glass set by Ville Seppanen <http://opengameart.org/content/gem-jewel-diamond-glass>.
* The leveleditor had been integrated in the game instead of being a separate program.
* Saving/loading has been implemented (used for checkpoints).
* CMake is now used.
* Transparency support for gfx.
* Checkpoints have been added.
* Menu for the leveleditor was made.
* The ImageManager has been added to prevent loading the same image twice.
* Swap block has been added.
* Fragile block has been added.
* New levelformat.
* Moving blocks and spikes have been added.
* Portal, Switch and Button added.
* Custom image support for blocks (never used and remove in MeAndMyShadow 0.3).
* Conveyor Belt added.
\ No newline at end of file
diff --git a/data/locale/hu.po b/data/locale/hu.po
index 548414a..b84ec98 100644
--- a/data/locale/hu.po
+++ b/data/locale/hu.po
@@ -1,2106 +1,2106 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the meandmyshadow package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: meandmyshadow 0.5svn\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-09-09 16:18+0800\n"
-"PO-Revision-Date: 2018-09-21 08:31+0000\n"
+"PO-Revision-Date: 2018-10-10 05:55+0000\n"
"Last-Translator: SanskritFritz <SanskritFritz+github@gmail.com>\n"
"Language-Team: Hungarian <https://hosted.weblate.org/projects/"
"me-and-my-shadow/translations/hu/>\n"
"Language: hu\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
-"X-Generator: Weblate 3.2-dev\n"
+"X-Generator: Weblate 3.2.1\n"
#: ../src/AchievementList.cpp:43
msgid "Newbie"
msgstr "Újonc"
#: ../src/AchievementList.cpp:43
msgid "Complete a level."
msgstr "Szint teljesítése."
#: ../src/AchievementList.cpp:44
msgid "Experienced player"
msgstr "Tapasztalt játékos"
#: ../src/AchievementList.cpp:44
msgid "Complete 50 levels."
msgstr "50 szint teljesítése."
#: ../src/AchievementList.cpp:45
msgid "Good job!"
msgstr "Szép volt!"
#: ../src/AchievementList.cpp:45
msgid "Receive a gold medal."
msgstr "Nyerj aranyérmet."
#: ../src/AchievementList.cpp:46
msgid "Expert"
msgstr "Szakértő"
#: ../src/AchievementList.cpp:46
msgid "Earn 50 gold medal."
msgstr "Nyerj 50 aranyérmet."
#: ../src/AchievementList.cpp:48
msgid "Graduate"
msgstr "Végzős"
#: ../src/AchievementList.cpp:48
msgid "Complete the tutorial level pack."
msgstr "Teljesítsd a oktató szintcsomagot."
#: ../src/AchievementList.cpp:49
msgid "Outstanding graduate"
msgstr "Kiváló végzős"
#: ../src/AchievementList.cpp:49
msgid "Complete the tutorial level pack with gold for all levels."
msgstr "Teljesítsd az oktató szintcsomagot aranyéremmel minden szinten."
#: ../src/AchievementList.cpp:51
msgid "Hooked"
msgstr "Ráharaptál"
#: ../src/AchievementList.cpp:51
msgid "Play Me and My Shadow for more than 2 hours."
msgstr "Több mint 2 óra Me and My Shadow játék."
#: ../src/AchievementList.cpp:52
msgid "Loyal fan of Me and My Shadow"
msgstr "A Me and My Shadow hű rajongója"
#: ../src/AchievementList.cpp:52
msgid "Play Me and My Shadow for more than 24 hours."
msgstr "Több mint 24 óra Me and My Shadow játék."
#: ../src/AchievementList.cpp:54
msgid "Constructor"
msgstr "Építész"
#: ../src/AchievementList.cpp:54
msgid "Use the level editor for more than 2 hours."
msgstr "Több mint 2 óra a szintszerkeszőben."
#: ../src/AchievementList.cpp:55
msgid "The creator"
msgstr "Az akotó"
#: ../src/AchievementList.cpp:55
msgid "Use the level editor for more than 24 hours."
msgstr "Több mint 24 óra a szintszerkeszőben."
#: ../src/AchievementList.cpp:57
msgid "Look, cute level!"
msgstr "Nocsak, cuki szint!"
#: ../src/AchievementList.cpp:57
msgid "Create a level for the first time."
msgstr "Új szint léthehozása első alkalommal."
#: ../src/AchievementList.cpp:58
msgid "The level museum"
msgstr "A szintmúzeum"
#: ../src/AchievementList.cpp:58
msgid "Create 50 levels."
msgstr "50 szint létrehozása."
#: ../src/AchievementList.cpp:60
msgid "Hello, World!"
msgstr "Szia világ!"
#: ../src/AchievementList.cpp:60
msgid "Write a script for the first time."
msgstr "Szkript írása első alkalommal."
#: ../src/AchievementList.cpp:62
msgid "Frog"
msgstr "Béka"
#: ../src/AchievementList.cpp:62
msgid "Jump 1000 times."
msgstr "1000 ugrás."
#: ../src/AchievementList.cpp:64
msgid "Wanderer"
msgstr "Vándor"
#: ../src/AchievementList.cpp:64
msgid "Travel 100 meters."
msgstr "100 méternyi utazás."
#: ../src/AchievementList.cpp:65
msgid "Runner"
msgstr "Futó"
#: ../src/AchievementList.cpp:65
msgid "Travel 1 kilometer."
msgstr "1 kilóméter utazás."
#: ../src/AchievementList.cpp:66
msgid "Long distance runner"
msgstr "Hosszútávfutó"
#: ../src/AchievementList.cpp:66
msgid "Travel 10 kilometers."
msgstr "10 kilóméter utazás."
#: ../src/AchievementList.cpp:67
msgid "Marathon runner"
msgstr "Maratonfutó"
#: ../src/AchievementList.cpp:67
msgid "Travel 42,195 meters."
msgstr "42 195 méternyi utazás."
#: ../src/AchievementList.cpp:69
msgid "Be careful!"
msgstr "Óvatosan!"
#: ../src/AchievementList.cpp:69
msgid "Die for the first time."
msgstr "Az első halál."
#: ../src/AchievementList.cpp:70
msgid "It doesn't matter..."
msgstr "Nem számít..."
#: ../src/AchievementList.cpp:70
msgid "Die 50 times."
msgstr "50 halál."
#: ../src/AchievementList.cpp:71
msgid "Expert of trial and error"
msgstr "A próbálgatás szakértője"
#: ../src/AchievementList.cpp:71
msgid "Die 1000 times."
msgstr "1000 halál."
#: ../src/AchievementList.cpp:73
msgid "Keep an eye for moving blocks!"
msgstr "Figyelj a mozgó tömbökre!"
#: ../src/AchievementList.cpp:73
msgid "Get squashed for the first time."
msgstr "Szétlapítva első alkalommal."
#: ../src/AchievementList.cpp:74
msgid "Potato masher"
msgstr "Krumplipüréző"
#: ../src/AchievementList.cpp:74
msgid "Get squashed 50 times."
msgstr "Szétlapítva 50 alkalommal."
#: ../src/AchievementList.cpp:76
msgid "Double kill"
msgstr "Kettős halál"
#: ../src/AchievementList.cpp:76
msgid "Get both the player and the shadow dead."
msgstr "Haljon meg a játékos és az árnyéka is."
#: ../src/AchievementList.cpp:78
msgid "Bad luck"
msgstr "Pech"
#: ../src/AchievementList.cpp:78
msgid "Die 5 times in under 5 seconds."
msgstr "Halj meg 5-ször 5 másodpercen belül."
#: ../src/AchievementList.cpp:79
msgid "This level is too dangerous"
msgstr "Ez a szint túl veszélyes"
#: ../src/AchievementList.cpp:79
msgid "Die 10 times in under 5 seconds."
msgstr "Halj meg 10-szer 5 másodpercen belül."
#: ../src/AchievementList.cpp:81
msgid "You forgot your friend"
msgstr "Megfeledkeztél a barátodról"
#: ../src/AchievementList.cpp:81
msgid "Finish the level with the player or the shadow dead."
msgstr "A szint befejezése úgy, hogy a játékos, vagy az árnyéka meghal."
#: ../src/AchievementList.cpp:82
msgid "Just in time"
msgstr "Épp időben"
#: ../src/AchievementList.cpp:82
msgid "Reach the exit with the player and the shadow simultaneously."
msgstr "A játékos és az árnyéka egyszerre érjenek a kijárathoz."
#: ../src/AchievementList.cpp:84
msgid "Recorder"
msgstr "Felvevő"
#: ../src/AchievementList.cpp:84
msgid "Record 100 times."
msgstr "100 felvétel készítése."
#: ../src/AchievementList.cpp:85
msgid "Shadowmaster"
msgstr "Árnyékmester"
#: ../src/AchievementList.cpp:85
msgid "Record 1000 times."
msgstr "1000 felvétel készítése."
#: ../src/AchievementList.cpp:87
msgid "Switch puller"
msgstr "Váltókezelő"
#: ../src/AchievementList.cpp:87
msgid "Pull the switch 100 times."
msgstr "Váltó meghúzása 100 alkalommal."
#: ../src/AchievementList.cpp:88
msgid "The switch is broken!"
msgstr "Elromlott a váltó!"
#: ../src/AchievementList.cpp:88
msgid "Pull the switch 1000 times."
msgstr "Váltó meghúzása 1000 alkalommal."
#: ../src/AchievementList.cpp:90
msgid "Swapper"
msgstr "Cserélő"
#: ../src/AchievementList.cpp:90
msgid "Swap 100 times."
msgstr "Csere 100 alkalommal."
#: ../src/AchievementList.cpp:91
msgid "Player to shadow to player to shadow..."
msgstr "Játékosból árnyék, árnyékból játékos..."
#: ../src/AchievementList.cpp:91
msgid "Swap 1000 times."
msgstr "Csere 1000 alkalommal."
#: ../src/AchievementList.cpp:93
msgid "Play it save"
msgstr "Biztos ami biztos"
#: ../src/AchievementList.cpp:93
msgid "Save 1000 times."
msgstr "Mentés 1000 alkalommal."
#: ../src/AchievementList.cpp:94
msgid "This game is too hard"
msgstr "Ez a játék túl nehéz"
#: ../src/AchievementList.cpp:94
msgid "Load the game 1000 times."
msgstr "Játék betöltése 1000 alkalommal."
#: ../src/AchievementList.cpp:96
msgid "No, thanks"
msgstr "Kösz, nem"
#: ../src/AchievementList.cpp:96
msgid "Complete a level with checkpoint, but without saving."
msgstr "Szint befejezése ellenőrző ponttal, de mentés nélkül."
#: ../src/AchievementList.cpp:98
msgid "Panic save"
msgstr "Pánik mentés"
#: ../src/AchievementList.cpp:98
msgid "Save twice in 1 second."
msgstr "2 mentés 1 másodpercen belül."
#: ../src/AchievementList.cpp:99
msgid "Panic load"
msgstr "Pánik betöltés"
#: ../src/AchievementList.cpp:99
msgid "Load twice in 1 second."
msgstr "2 betöltés 1 másodpercen belül."
#: ../src/AchievementList.cpp:101
msgid "Bad saving position"
msgstr "Rossz mentési pozíció"
#: ../src/AchievementList.cpp:101
msgid "Load the game and die within 1 second."
msgstr "Játék betöltése és halál 1 másodpercen belül."
#: ../src/AchievementList.cpp:102
msgid "This level is too hard"
msgstr "Ez a szint túl nehéz"
#: ../src/AchievementList.cpp:102
msgid "Load the same save and die 100 times."
msgstr "Adott mentés betöltése és halál 100 alkalommal."
#: ../src/AchievementList.cpp:104
msgid "Quick swap"
msgstr "Gyorsváltás"
#: ../src/AchievementList.cpp:104
msgid "Swap twice in under a second."
msgstr "Váltás kétszer egy másodpercen belül."
#: ../src/AchievementList.cpp:107
msgid "Horizontal confusion"
msgstr "Vízszintes összezavarodás"
#: ../src/AchievementList.cpp:107
msgid "Press left and right simultaneously."
msgstr "Nyomd meg egyszerre a balra és a jobbra gombot."
#: ../src/AchievementList.cpp:109
msgid "Cheater"
msgstr "Csaló"
#: ../src/AchievementList.cpp:109
msgid "Cheat in game."
msgstr "Csalás a játékban."
#: ../src/AchievementList.cpp:111
msgid "Programmer"
msgstr "Programozó"
#: ../src/AchievementList.cpp:111
msgid "Play the development version of Me and My Shadow."
msgstr "Játék a Me and My Shadow fejlesztői változatával."
#: ../src/Addons.cpp:44 ../src/LevelPackManager.cpp:108
msgid "Levels"
msgstr "Szintek"
#: ../src/Addons.cpp:44
msgid "Single level which usually contain demanding puzzles"
msgstr "Egyetlen szint, mely általában igényes rejtvényeket tartalmaz"
#: ../src/Addons.cpp:45
msgid "Levelpacks"
msgstr "Szintcsomagok"
#: ../src/Addons.cpp:45
msgid "Collection of levels with the same author or style"
msgstr "Szintek gyűjteménye, adott szerzőtől vagy stílusban"
#: ../src/Addons.cpp:46
msgid "Themes"
msgstr "Témák"
#: ../src/Addons.cpp:46
msgid "Give every block and background a new look and feel"
msgstr "A tömbök és a háttér új külalakot kap"
#: ../src/Addons.cpp:55 ../src/TitleMenu.cpp:46
msgid "Addons"
msgstr "Bővítmények"
#: ../src/Addons.cpp:87
msgid "Unable to initialize addon menu:"
msgstr "Nem tudtam az alábbi bővítmény-menüt elindítani:"
#: ../src/Addons.cpp:95 ../src/Addons.cpp:158 ../src/Addons.cpp:662
#: ../src/Addons.cpp:690 ../src/CreditsMenu.cpp:89 ../src/LevelSelect.cpp:168
#: ../src/StatisticsScreen.cpp:159
msgid "Back"
msgstr "Vissza"
#: ../src/Addons.cpp:169
msgid "ERROR: unable to download addons file!"
msgstr "HIBA: a bővítmények letöltése sikertelen!"
# TRANSLATORS: addon_list is the name of a file and should not be translated.
#: ../src/Addons.cpp:182
msgid "ERROR: unable to load addon_list file!"
msgstr "HIBA: az addon_list fájl letöltése sikertelen!"
#: ../src/Addons.cpp:193
msgid "ERROR: Invalid file format of addons file!"
msgstr "HIBA: a bővítmény fájl formátuma nem jó!"
#: ../src/Addons.cpp:205
msgid "ERROR: Addon list version is unsupported!"
msgstr "HIBA: nem támogatott addon_list verzió!"
# TRANSLATORS: installed_addons is the name of a file and should not be translated.
#: ../src/Addons.cpp:226
msgid "ERROR: Unable to create the installed_addons file."
msgstr "HIBA: az installed_addons fájl létrehozása sikertelen."
#: ../src/Addons.cpp:238
msgid "ERROR: Invalid file format of the installed_addons!"
msgstr "HIBA: az installed_addons fájl formátuma nem jó!"
# TRANSLATORS: indicates the author of an addon.
#: ../src/Addons.cpp:389 ../src/Addons.cpp:621
#, c-format
msgid "by %s"
msgstr "írta: %s"
#: ../src/Addons.cpp:397
msgid "Installed"
msgstr "Telepítve"
#: ../src/Addons.cpp:402
msgid "Updatable"
msgstr "Frissíthető"
#: ../src/Addons.cpp:412
msgid "Not installed"
msgstr "Nincs telepítve"
#: ../src/Addons.cpp:625
#, c-format
msgid "Version: %d\n"
msgstr "Verzió: %d\n"
#: ../src/Addons.cpp:627
#, c-format
msgid "Installed version: %d\n"
msgstr "Telepített verzió: %d\n"
#: ../src/Addons.cpp:630
#, c-format
msgid "License: %s\n"
msgstr "Licensz: %s\n"
#: ../src/Addons.cpp:633
#, c-format
msgid "Website: %s\n"
msgstr "Weboldal: %s\n"
#: ../src/Addons.cpp:637
msgid "(No descriptions provided)"
msgstr "(Nincs hozzá leírás)"
#: ../src/Addons.cpp:657 ../src/Addons.cpp:684
msgid "Remove"
msgstr "Eltávolítás"
#: ../src/Addons.cpp:673
msgid "Update"
msgstr "Frissítés"
#: ../src/Addons.cpp:679
msgid "Install"
msgstr "Telepítés"
#: ../src/Addons.cpp:774
#, c-format
msgid "This addon can't be removed because it's needed by %s."
msgstr "Ez a bővítményt nem lehet eltávolítani, mert szükséges ehhez: %s."
#: ../src/Addons.cpp:774 ../src/Addons.cpp:1051
msgid "Dependency"
msgstr "Függőség"
#: ../src/Addons.cpp:803
#, c-format
msgid "WARNING: File '%s' appears to have been removed already."
msgstr "FIGYELMEZTETÉS: úgy tűnik, '%s' már el lett távolítva."
#: ../src/Addons.cpp:803 ../src/Addons.cpp:810 ../src/Addons.cpp:818
#: ../src/Addons.cpp:825 ../src/Addons.cpp:834 ../src/Addons.cpp:840
#: ../src/Addons.cpp:859 ../src/Addons.cpp:866 ../src/Addons.cpp:893
#: ../src/Addons.cpp:900 ../src/Addons.cpp:907 ../src/Addons.cpp:918
#: ../src/Addons.cpp:947 ../src/Addons.cpp:952 ../src/Addons.cpp:962
#: ../src/Addons.cpp:968 ../src/Addons.cpp:981 ../src/Addons.cpp:986
#: ../src/Addons.cpp:1008 ../src/Addons.cpp:1014 ../src/Addons.cpp:1044
msgid "Addon error"
msgstr "Bővítmény hiba"
#: ../src/Addons.cpp:810
#, c-format
msgid "ERROR: Unable to remove file '%s'!"
msgstr "HIBA: nem sikerült törölni a '%s' fájlt!"
#: ../src/Addons.cpp:818
#, c-format
msgid "WARNING: Directory '%s' appears to have been removed already."
msgstr "FIGYELMEZTETÉS: A '%s' könyvtárat már törölték."
#: ../src/Addons.cpp:825
#, c-format
msgid "ERROR: Unable to remove directory '%s'!"
msgstr "HIBA: Nem sikerült törölni a '%s' könyvtárat!"
#: ../src/Addons.cpp:834
#, c-format
msgid "WARNING: Level '%s' appears to have been removed already."
msgstr "FIGYELMEZTETÉS: A '%s' szint már törölve van."
#: ../src/Addons.cpp:840
#, c-format
msgid "ERROR: Unable to remove level '%s'!"
msgstr "HIBA: Nem sikerült törölni a '%s' szintet!"
#: ../src/Addons.cpp:859
#, c-format
msgid "WARNING: Levelpack directory '%s' appears to have been removed already."
msgstr "FIGYELMEZTETÉS: A szintcsomag '%s' könyvtára már törölve van."
#: ../src/Addons.cpp:866
#, c-format
msgid "ERROR: Unable to remove levelpack directory '%s'!"
msgstr "HIBA: Nem sikerült törölni a szintcsomag '%s' könyvtárat!"
#: ../src/Addons.cpp:893
#, c-format
msgid "ERROR: Unable to download addon file %s."
msgstr "HIBA: Nem sikerült letölteni a '%s' bővítmény fájlt."
#: ../src/Addons.cpp:900
#, c-format
msgid "ERROR: Unable to extract addon file %s."
msgstr "HIBA: Nem sikerült kibontani a '%s' bővítmény fáljt."
#: ../src/Addons.cpp:907
msgid "ERROR: Addon is missing metadata!"
msgstr "HIBA: A bővítmény nem tartalmaz meta-adatokat!"
#: ../src/Addons.cpp:918
msgid "ERROR: Invalid file format for metadata file!"
msgstr "HIBA: Érvénytelen meta-adat fáljformátum!"
#: ../src/Addons.cpp:947
#, c-format
msgid "WARNING: File '%s' already exists, addon may be broken or not working!"
msgstr ""
"FIGYELMEZTETÉS: A '%s' fájl már létezik, a bővitmény hibás, vagy nem működik!"
#: ../src/Addons.cpp:952
#, c-format
msgid ""
"WARNING: Unable to copy file '%s' to '%s', addon may be broken or not "
"working!"
msgstr ""
"FIGYELMEZTETÉS: A '%s' fájlt nem sikerült ide másolni: '%s', a bővítmény "
"hibás vagy nem működik!"
#: ../src/Addons.cpp:962
#, c-format
msgid ""
"WARNING: Destination directory '%s' already exists, addon may be broken or "
"not working!"
msgstr ""
"FIGYELMEZTETÉS: A '%s' célkönyvtár már létezik, a bővitmény hibás, vagy nem "
"működik!"
#: ../src/Addons.cpp:968 ../src/Addons.cpp:1014
#, c-format
msgid ""
"WARNING: Unable to move directory '%s' to '%s', addon may be broken or not "
"working!"
msgstr ""
"FIGYELMEZTETÉS: A '%s' könyvtárt nem sikerült ide másolni: '%s', a "
"bővítmény hibás vagy nem működik!"
#: ../src/Addons.cpp:981
#, c-format
msgid "WARNING: Level '%s' already exists, addon may be broken or not working!"
msgstr ""
"FIGYELMEZTETÉS: A '%s' szint már létezik, a bővitmény hibás, vagy nem "
"működik!"
#: ../src/Addons.cpp:986
#, c-format
msgid ""
"WARNING: Unable to copy level '%s' to '%s', addon may be broken or not "
"working!"
msgstr ""
"FIGYELMEZTETÉS: A '%s' szintet nem sikerült ide másolni: '%s', a bővítmény "
"hibás vagy nem működik!"
#: ../src/Addons.cpp:1008
#, c-format
msgid ""
"WARNING: Levelpack directory '%s' already exists, addon may be broken or not "
"working!"
msgstr ""
"FIGYELMEZTETÉS: A '%s' szintcsomag könyvtára már létezik, a bővitmény hibás, "
"vagy nem működik!"
#: ../src/Addons.cpp:1044
#, c-format
msgid "ERROR: Addon requires another addon (%s) which can't be found!"
msgstr "HIBA: A bővítmény másik bővítményt (%s) igényel, de az nem található!"
#: ../src/Addons.cpp:1051
#, c-format
msgid "The addon %s is needed and will be installed now."
msgstr "A %s bővítményre szükség van, és telepítve lesz most."
#: ../src/Block.cpp:822 ../src/LevelEditor.cpp:265
msgid "On"
msgstr "Be"
#: ../src/Block.cpp:823 ../src/LevelEditor.cpp:266
msgid "Off"
msgstr "Ki"
#: ../src/CommandManager.cpp:41
#, c-format
msgid "Undo %s"
msgstr "%s visszavonása"
#: ../src/CommandManager.cpp:43
msgid "Can't undo"
msgstr "Nem tudom visszavonni"
#: ../src/CommandManager.cpp:49
#, c-format
msgid "Redo %s"
msgstr "%s újra"
#: ../src/CommandManager.cpp:51
msgid "Can't redo"
msgstr "Nem lehet újra csinálni"
# TRANSLATORS: Context: Undo/Redo ...
#: ../src/Commands.cpp:190
msgid "Resize level"
msgstr "Szint átméretezése"
# TRANSLATORS: Context: Undo/Redo ...
#: ../src/Commands.cpp:807
msgid "Modify level property"
msgstr "Szint tulajdonság módosítása"
# TRANSLATORS: Context: Undo/Redo ...
#: ../src/Commands.cpp:919
#, c-format
msgid "Add scenery layer %s"
msgstr "Táj réteg %s hozzáadása"
# TRANSLATORS: Context: Undo/Redo ...
#: ../src/Commands.cpp:921
#, c-format
msgid "Delete scenery layer %s"
msgstr "Táj réteg %s törlése"
# TRANSLATORS: Context: Undo/Redo ...
#: ../src/Commands.cpp:951
#, c-format
msgid "Modify the property of scenery layer %s"
msgstr "Táj réteg tulajdonság %s módosítása"
# TRANSLATORS: Context: Undo/Redo ...
#: ../src/Commands.cpp:1040
#, c-format
msgid "Move %d object from layer %s to layer %s"
msgid_plural "Move %d objects from layer %s to layer %s"
msgstr[0] "%d objektum átmozgatása %s rétegről %s rétegre"
msgstr[1] "%d objektum átmozgatása %s rétegről %s rétegre"
#: ../src/CreditsMenu.cpp:35 ../src/TitleMenu.cpp:53
msgid "Credits"
msgstr ""
# TRANSLATORS: Font used in GUI:
# - Use "knewave" for languages using Latin and Latin-derived alphabets
# - "DroidSansFallback" can be used for non-Latin writing systems
#: ../src/Functions.cpp:569 ../src/Functions.cpp:570 ../src/Functions.cpp:571
#: ../src/Functions.cpp:588
msgid "knewave"
msgstr "knewave"
# TRANSLATORS: Font used for normal text:
# - Use "Blokletters-Viltstift" for languages using Latin and Latin-derived alphabets
# - "DroidSansFallback" can be used for non-Latin writing systems
#: ../src/Functions.cpp:575
msgid "Blokletters-Viltstift"
msgstr "Blokletters-Viltstift"
#: ../src/Functions.cpp:674
msgid "Loading..."
msgstr "Betöltés..."
#: ../src/Functions.cpp:1243 ../src/Functions.cpp:1270
#: ../src/LevelEditor.cpp:559 ../src/LevelEditor.cpp:693
#: ../src/LevelEditor.cpp:758 ../src/LevelEditor.cpp:821
#: ../src/LevelEditor.cpp:908 ../src/LevelEditor.cpp:1033
#: ../src/LevelEditor.cpp:1083 ../src/LevelEditor.cpp:1180
#: ../src/LevelEditor.cpp:1244 ../src/LevelEditor.cpp:2923
#: ../src/LevelEditSelect.cpp:244 ../src/LevelEditSelect.cpp:277
#: ../src/LevelEditSelect.cpp:317
msgid "OK"
msgstr "OK"
#: ../src/Functions.cpp:1244 ../src/Functions.cpp:1256
#: ../src/Functions.cpp:1266 ../src/LevelEditor.cpp:565
#: ../src/LevelEditor.cpp:699 ../src/LevelEditor.cpp:764
#: ../src/LevelEditor.cpp:827 ../src/LevelEditor.cpp:914
#: ../src/LevelEditor.cpp:1039 ../src/LevelEditor.cpp:1089
#: ../src/LevelEditor.cpp:1186 ../src/LevelEditor.cpp:1250
#: ../src/LevelEditor.cpp:2929 ../src/LevelEditSelect.cpp:248
#: ../src/LevelEditSelect.cpp:281 ../src/LevelEditSelect.cpp:321
#: ../src/OptionsMenu.cpp:289
msgid "Cancel"
msgstr "Mégse"
#: ../src/Functions.cpp:1248
msgid "Abort"
msgstr "Megszakít"
#: ../src/Functions.cpp:1249 ../src/Functions.cpp:1265
msgid "Retry"
msgstr "Újra"
#: ../src/Functions.cpp:1250
msgid "Ignore"
msgstr "Mellőz"
#: ../src/Functions.cpp:1254 ../src/Functions.cpp:1260
msgid "Yes"
msgstr "Igen"
#: ../src/Functions.cpp:1255 ../src/Functions.cpp:1261
msgid "No"
msgstr "Nem"
# TRANSLATORS: Please do not remove %s or %d from your translation:
# - %d means the level number in a levelpack
# - %s means the name of current level
#: ../src/Game.cpp:280 ../src/Game.cpp:1236
#, c-format
msgid "Level %d %s"
msgstr "%d %s szint"
# TRANSLATORS: Please do not remove %s from your translation:
# - %s will be replaced with current action key
#: ../src/Game.cpp:915
#, c-format
msgid "Press %s key to save the game."
msgstr "Nyomd meg a %s gombot, hogy mentést készíts a játékról."
# TRANSLATORS: Please do not remove %s from your translation:
# - %s will be replaced with current action key
#: ../src/Game.cpp:920
#, c-format
msgid "Press %s key to swap the position of player and shadow."
msgstr "Nyomd meg a %s gombot, hogy a játékos és árnyéka helyet cseréljen."
# TRANSLATORS: Please do not remove %s from your translation:
# - %s will be replaced with current action key
#: ../src/Game.cpp:925
#, c-format
msgid "Press %s key to activate the switch."
msgstr "Nyomd meg a %s gombot, hogy a kapcsolót aktiváld."
# TRANSLATORS: Please do not remove %s from your translation:
# - %s will be replaced with current action key
#: ../src/Game.cpp:930
#, c-format
msgid "Press %s key to teleport."
msgstr "Nyomd meg a %s gombot a teleportáláshoz."
# TRANSLATORS: Please do not remove %s from your translation:
# - first %s means currently configured key to restart game
# - Second %s means configured key to load from last save
#: ../src/Game.cpp:972
#, c-format
msgid "Press %s to restart current level or press %s to load the game."
-msgstr ""
+msgstr "%s újraindítja a szintet, %s újratölti a játékot."
# TRANSLATORS: Please do not remove %s from your translation:
# - %s will be replaced with currently configured key to restart game
#: ../src/Game.cpp:983
#, c-format
msgid "Press %s to restart current level."
-msgstr ""
+msgstr "%s újraindítja a szintet."
#: ../src/Game.cpp:996
msgid "Your shadow has died."
-msgstr ""
+msgstr "Az árnyékod meghalt."
#: ../src/Game.cpp:1052
#, c-format
msgid "%d recording"
msgid_plural "%d recordings"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "%d felvétel"
+msgstr[1] "%d felvétel"
#: ../src/Game.cpp:1224
msgid "You've finished:"
-msgstr ""
+msgstr "Elvégezted:"
# TRANSLATORS: Please do not remove %-.2f from your translation:
# - %-.2f means time in seconds
# - s is shortened form of a second. Try to keep it so.
#: ../src/Game.cpp:1291
#, c-format
msgid "Time: %-.2fs"
-msgstr ""
+msgstr "Idő: %-.2f mp"
# TRANSLATORS: Please do not remove %-.2f from your translation:
# - %-.2f means time in seconds
# - s is shortened form of a second. Try to keep it so.
#: ../src/Game.cpp:1300
#, c-format
msgid "Best time: %-.2fs"
-msgstr ""
+msgstr "Legjobb idő: %-.2f mp"
#: ../src/Game.cpp:1311
#, c-format
msgid "Target time: %-.2fs"
-msgstr ""
+msgstr "Célidő: %-.2f mp"
# TRANSLATORS: Please do not remove %d from your translation:
# - %d means the number of recordings user has made
#: ../src/Game.cpp:1332
#, c-format
msgid "Recordings: %d"
-msgstr ""
+msgstr "Felvétel: %d"
# TRANSLATORS: Please do not remove %d from your translation:
# - %d means the number of recordings user has made
#: ../src/Game.cpp:1340
#, c-format
msgid "Best recordings: %d"
-msgstr ""
+msgstr "Legjobb felvételek: %d"
#: ../src/Game.cpp:1350
#, c-format
msgid "Target recordings: %d"
-msgstr ""
+msgstr "Célfelvétel: %d"
# TRANSLATORS: Please do not remove %s from your translation:
# - %s will be replaced with name of a prize medal (gold, silver or bronze)
#: ../src/Game.cpp:1363
#, c-format
msgid "You earned the %s medal"
-msgstr ""
+msgstr "Eddig %s medált nyertél"
#: ../src/Game.cpp:1363
msgid "GOLD"
-msgstr ""
+msgstr "ARANY"
#: ../src/Game.cpp:1363
msgid "SILVER"
-msgstr ""
+msgstr "EZÜST"
#: ../src/Game.cpp:1363
msgid "BRONZE"
-msgstr ""
+msgstr "BRONZ"
# TRANSLATORS: used as return to the level selector menu
#: ../src/Game.cpp:1390
msgid "Menu"
-msgstr ""
+msgstr "Menü"
# TRANSLATORS: used as restart level
#: ../src/Game.cpp:1397 ../src/InputManager.cpp:47
msgid "Restart"
-msgstr ""
+msgstr "Újraindítás"
# TRANSLATORS: used as next level
#: ../src/Game.cpp:1404
msgid "Next"
-msgstr ""
+msgstr "Következő"
#: ../src/Game.cpp:1430
msgid "Game replay is done."
-msgstr ""
+msgstr "Ismétlés vége."
#: ../src/Game.cpp:1430
msgid "Game Replay"
-msgstr ""
+msgstr "Ismétlés"
#: ../src/Game.cpp:1767 ../src/Game.cpp:1769
msgid "Congratulations"
-msgstr ""
+msgstr "Gratulálok"
#: ../src/Game.cpp:1769
msgid "You have finished the levelpack!"
-msgstr ""
+msgstr "Sikerült teljesítened a szintcsomagot!"
#: ../src/InputManager.cpp:46
msgid "Up (in menu)"
-msgstr ""
+msgstr "Fel (menüben)"
#: ../src/InputManager.cpp:46
msgid "Down (in menu)"
-msgstr ""
+msgstr "Le (menüben)"
#: ../src/InputManager.cpp:46
msgid "Left"
-msgstr ""
+msgstr "Balra"
#: ../src/InputManager.cpp:46
msgid "Right"
-msgstr ""
+msgstr "Jobbra"
#: ../src/InputManager.cpp:46
msgid "Jump"
-msgstr ""
+msgstr "Ugrás"
#: ../src/InputManager.cpp:46
msgid "Action"
-msgstr ""
+msgstr "Akció"
#: ../src/InputManager.cpp:46
msgid "Space (Record)"
-msgstr ""
+msgstr "Szóköz (Felvétel)"
#: ../src/InputManager.cpp:46
msgid "Cancel recording"
-msgstr ""
+msgstr "Felvétel megszakítása"
#: ../src/InputManager.cpp:47
msgid "Escape"
-msgstr ""
+msgstr "Megszakítás"
#: ../src/InputManager.cpp:47
msgid "Tab (View shadow/Level prop.)"
-msgstr ""
+msgstr "Tab (Árnyék mutatása/Szint tul.)"
#: ../src/InputManager.cpp:47
msgid "Save game (in editor)"
-msgstr ""
+msgstr "Játék mentése (a szerkesztőben)"
#: ../src/InputManager.cpp:47
msgid "Load game"
-msgstr ""
+msgstr "Játék betöltése"
#: ../src/InputManager.cpp:47
msgid "Swap (in editor)"
-msgstr ""
+msgstr "Csere (a szerkesztőben)"
#: ../src/InputManager.cpp:48
msgid "Teleport (in editor)"
-msgstr ""
+msgstr "Teleportálás (a szerkesztőben)"
#: ../src/InputManager.cpp:48
msgid "Suicide (in editor)"
-msgstr ""
+msgstr "Öngyilkosság (a szerkesztőben)"
#: ../src/InputManager.cpp:48
msgid "Shift (in editor)"
-msgstr ""
+msgstr "Futószalag (a szerkesztőben)"
#: ../src/InputManager.cpp:48
msgid "Next block type (in Editor)"
-msgstr ""
+msgstr "Következő blokktípus (a szerkesztőben)"
#: ../src/InputManager.cpp:49
msgid "Previous block type (in editor)"
-msgstr ""
+msgstr "Előző blokktípus (a szerkesztőben)"
#: ../src/InputManager.cpp:49
msgid "Select (in menu)"
-msgstr ""
+msgstr "Kiválasztás (a menüben)"
# TRANSLAOTRS: This is used when the name of the key code is not found.
#: ../src/InputManager.cpp:156
#, c-format
msgid "(Key %d)"
-msgstr ""
+msgstr "(%d billentyű)"
#: ../src/InputManager.cpp:163
#, c-format
msgid "Joystick axis %d %s"
-msgstr ""
+msgstr "Joystick tengely %d %s"
#: ../src/InputManager.cpp:166
#, c-format
msgid "Joystick button %d"
-msgstr ""
+msgstr "Joystick gomb %d"
#: ../src/InputManager.cpp:171
#, c-format
msgid "Joystick hat %d left"
-msgstr ""
+msgstr "Joystick kontroll %d balra"
#: ../src/InputManager.cpp:174
#, c-format
msgid "Joystick hat %d right"
-msgstr ""
+msgstr "Joystick kontroll %d jobbra"
#: ../src/InputManager.cpp:177
#, c-format
msgid "Joystick hat %d up"
-msgstr ""
+msgstr "Joystick kontroll %d fel"
#: ../src/InputManager.cpp:180
#, c-format
msgid "Joystick hat %d down"
-msgstr ""
+msgstr "Joystick kontroll %d le"
# TRANSLAOTRS: This is used when the JOYSTICK_HAT value is invalid.
#: ../src/InputManager.cpp:185
#, c-format
msgid "Joystick hat %d %d"
-msgstr ""
+msgstr "Joystick kontroll %d %d"
#: ../src/InputManager.cpp:202
msgid "OR"
-msgstr ""
+msgstr "VAGY"
#: ../src/InputManager.cpp:416
msgid "Select an item and press a key to change it."
-msgstr ""
+msgstr "Válassz ki egy elemet és nyomj meg egy gombot hogy megváltoztasd."
#: ../src/InputManager.cpp:419
msgid "Press backspace to clear the selected item."
-msgstr ""
+msgstr "Visszatörlés gombbal lehet törölni az adott elem tartalmát."
#: ../src/LevelEditor.cpp:56
msgid "Block"
-msgstr ""
+msgstr "Blokk"
#: ../src/LevelEditor.cpp:56
msgid "Player Start"
-msgstr ""
+msgstr "Játékos kezdőpont"
#: ../src/LevelEditor.cpp:56
msgid "Shadow Start"
-msgstr ""
+msgstr "Árnyék kezdőpont"
#: ../src/LevelEditor.cpp:57
msgid "Exit"
-msgstr ""
+msgstr "Kijárat"
#: ../src/LevelEditor.cpp:57
msgid "Shadow Block"
-msgstr ""
+msgstr "Árnyék blokk"
#: ../src/LevelEditor.cpp:57
msgid "Spikes"
-msgstr ""
+msgstr "Tüskék"
#: ../src/LevelEditor.cpp:58
msgid "Checkpoint"
-msgstr ""
+msgstr "Ellenőrző pont"
#: ../src/LevelEditor.cpp:58 ../src/LevelEditSelect.cpp:312
msgid "Swap"
-msgstr ""
+msgstr "Csere"
#: ../src/LevelEditor.cpp:58
msgid "Fragile"
msgstr ""
#: ../src/LevelEditor.cpp:59
msgid "Moving Block"
msgstr ""
#: ../src/LevelEditor.cpp:59
msgid "Moving Shadow Block"
msgstr ""
#: ../src/LevelEditor.cpp:59
msgid "Moving Spikes"
msgstr ""
#: ../src/LevelEditor.cpp:60
msgid "Teleporter"
msgstr ""
#: ../src/LevelEditor.cpp:60
msgid "Button"
msgstr ""
#: ../src/LevelEditor.cpp:60
msgid "Switch"
msgstr ""
#: ../src/LevelEditor.cpp:61
msgid "Conveyor Belt"
msgstr ""
#: ../src/LevelEditor.cpp:61
msgid "Shadow Conveyor Belt"
msgstr ""
#: ../src/LevelEditor.cpp:61
msgid "Notification Block"
msgstr ""
#: ../src/LevelEditor.cpp:61
msgid "Collectable"
msgstr ""
#: ../src/LevelEditor.cpp:61
msgid "Pushable"
msgstr ""
#: ../src/LevelEditor.cpp:65 ../src/LevelEditor.cpp:310
msgid "Select"
msgstr ""
#: ../src/LevelEditor.cpp:65
msgid "Add"
msgstr ""
#: ../src/LevelEditor.cpp:65 ../src/LevelEditor.cpp:311
msgid "Delete"
msgstr ""
#: ../src/LevelEditor.cpp:65 ../src/LevelPlaySelect.cpp:66
#: ../src/TitleMenu.cpp:43
msgid "Play"
msgstr ""
#: ../src/LevelEditor.cpp:65 ../src/LevelEditor.cpp:2852
msgid "Level settings"
msgstr ""
#: ../src/LevelEditor.cpp:65
msgid "Save level"
msgstr ""
#: ../src/LevelEditor.cpp:65
msgid "Back to menu"
msgstr ""
#: ../src/LevelEditor.cpp:65
msgid "Configure"
msgstr ""
#: ../src/LevelEditor.cpp:84
#, c-format
msgid "%s (Scenery)"
msgstr ""
#: ../src/LevelEditor.cpp:267
msgid "Toggle"
msgstr ""
#: ../src/LevelEditor.cpp:270
msgid "Complete"
msgstr ""
#: ../src/LevelEditor.cpp:271
msgid "One step"
msgstr ""
#: ../src/LevelEditor.cpp:272
msgid "Two steps"
msgstr ""
#: ../src/LevelEditor.cpp:273
msgid "Gone"
msgstr ""
#: ../src/LevelEditor.cpp:291
msgid "Negative infinity"
msgstr ""
#: ../src/LevelEditor.cpp:293
msgid "Zero"
msgstr ""
#: ../src/LevelEditor.cpp:295
msgid "Level size"
msgstr ""
#: ../src/LevelEditor.cpp:297
msgid "Positive infinity"
msgstr ""
#: ../src/LevelEditor.cpp:299
msgid "Default"
msgstr ""
#: ../src/LevelEditor.cpp:308
msgid "Deselect"
msgstr ""
#: ../src/LevelEditor.cpp:318 ../src/LevelEditor.cpp:1136
#, c-format
msgid "Horizontal repeat start: %s"
msgstr ""
#: ../src/LevelEditor.cpp:320 ../src/LevelEditor.cpp:1137
#, c-format
msgid "Horizontal repeat end: %s"
msgstr ""
#: ../src/LevelEditor.cpp:322 ../src/LevelEditor.cpp:1138
#, c-format
msgid "Vertical repeat start: %s"
msgstr ""
#: ../src/LevelEditor.cpp:324 ../src/LevelEditor.cpp:1139
#, c-format
msgid "Vertical repeat end: %s"
msgstr ""
#: ../src/LevelEditor.cpp:329 ../src/LevelEditor.cpp:1150
msgid "Custom scenery"
msgstr ""
#: ../src/LevelEditor.cpp:335 ../src/LevelEditor.cpp:600
#: ../src/LevelEditor.cpp:602
msgid "Visible"
msgstr ""
#: ../src/LevelEditor.cpp:344
msgid "Link"
msgstr ""
#: ../src/LevelEditor.cpp:345
msgid "Remove Links"
msgstr ""
#: ../src/LevelEditor.cpp:349 ../src/LevelEditor.cpp:624
#: ../src/LevelEditor.cpp:626
msgid "Automatic"
msgstr ""
#: ../src/LevelEditor.cpp:359 ../src/LevelEditor.cpp:649
#, c-format
msgid "Behavior: %s"
msgstr ""
#: ../src/LevelEditor.cpp:362
msgid "Path"
msgstr ""
#: ../src/LevelEditor.cpp:363
msgid "Remove Path"
msgstr ""
#: ../src/LevelEditor.cpp:365 ../src/LevelEditor.cpp:371
#: ../src/LevelEditor.cpp:587 ../src/LevelEditor.cpp:589
msgid "Activated"
msgstr ""
#: ../src/LevelEditor.cpp:366 ../src/LevelEditor.cpp:612
#: ../src/LevelEditor.cpp:614
msgid "Looping"
msgstr ""
#: ../src/LevelEditor.cpp:372 ../src/LevelEditor.cpp:3526
msgid "Speed"
msgstr ""
#: ../src/LevelEditor.cpp:378 ../src/LevelEditor.cpp:668
#, c-format
msgid "State: %s"
msgstr ""
#: ../src/LevelEditor.cpp:382 ../src/LevelEditor.cpp:3511
msgid "Message"
msgstr ""
#: ../src/LevelEditor.cpp:384 ../src/LevelEditor.cpp:1202
#: ../src/LevelEditor.cpp:3825
msgid "Appearance"
msgstr ""
#: ../src/LevelEditor.cpp:389 ../src/LevelEditor.cpp:431
#: ../src/LevelEditor.cpp:715
msgid "Scripting"
msgstr ""
#: ../src/LevelEditor.cpp:402 ../src/LevelEditor.cpp:867
#: ../src/LevelEditor.cpp:885
#, c-format
msgid "Background layer: %s"
msgstr ""
#: ../src/LevelEditor.cpp:409 ../src/LevelEditor.cpp:866
#: ../src/LevelEditor.cpp:884
msgid "Blocks layer"
msgstr ""
#: ../src/LevelEditor.cpp:417 ../src/LevelEditor.cpp:867
#: ../src/LevelEditor.cpp:885
#, c-format
msgid "Foreground layer: %s"
msgstr ""
#: ../src/LevelEditor.cpp:423
msgid "Add new layer"
msgstr ""
#: ../src/LevelEditor.cpp:424
msgid "Delete selected layer"
msgstr ""
#: ../src/LevelEditor.cpp:425
msgid "Configure selected layer"
msgstr ""
#: ../src/LevelEditor.cpp:426
msgid "Move selected object to layer"
msgstr ""
#: ../src/LevelEditor.cpp:430 ../src/OptionsMenu.cpp:55
msgid "Settings"
msgstr ""
#: ../src/LevelEditor.cpp:463
msgid ""
"NOTE: the layers are sorted by name alphabetically.\n"
"The layer is background layer if its name is < 'f'\n"
"by dictionary order, otherwise it's foreground layer."
msgstr ""
#: ../src/LevelEditor.cpp:539
msgid "Notification block"
msgstr ""
#: ../src/LevelEditor.cpp:545
msgid "Enter message here:"
msgstr ""
#: ../src/LevelEditor.cpp:646
msgid "Behavior"
msgstr ""
#: ../src/LevelEditor.cpp:665
msgid "State"
msgstr ""
#: ../src/LevelEditor.cpp:673
msgid "Conveyor belt speed"
msgstr ""
#: ../src/LevelEditor.cpp:679
msgid "Enter speed here:"
msgstr ""
#: ../src/LevelEditor.cpp:690
msgid "NOTE: 1 Speed = 0.08 block/s"
msgstr ""
#: ../src/LevelEditor.cpp:721
msgid "Id:"
msgstr ""
#: ../src/LevelEditor.cpp:787
msgid "Level Scripting"
msgstr ""
#: ../src/LevelEditor.cpp:892
msgid "Add layer"
msgstr ""
#: ../src/LevelEditor.cpp:898
msgid "Enter the layer name:"
msgstr ""
#: ../src/LevelEditor.cpp:943
#, c-format
msgid "Are you sure you want to delete layer '%s'?"
msgstr ""
#: ../src/LevelEditor.cpp:944
msgid "Delete layer"
msgstr ""
#: ../src/LevelEditor.cpp:968
msgid "Layer settings"
msgstr ""
#: ../src/LevelEditor.cpp:974
msgid "Layer name:"
msgstr ""
#: ../src/LevelEditor.cpp:989
msgid "Layer moving speed (1 speed = 0.8 block/s):"
msgstr ""
#: ../src/LevelEditor.cpp:1010
msgid "Speed of following camera:"
msgstr ""
#: ../src/LevelEditor.cpp:1062
msgid "Move to layer"
msgstr ""
#: ../src/LevelEditor.cpp:1068
msgid "Enter the layer name (create new layer if necessary):"
msgstr ""
#: ../src/LevelEditor.cpp:1132
msgid "Repeat mode"
msgstr ""
#: ../src/LevelEditor.cpp:1156
msgid "Custom scenery:"
msgstr ""
#: ../src/LevelEditor.cpp:1219
msgid "(Use the default appearance for this block)"
msgstr ""
# TRANSLATORS: Block name
# TRANSLATORS: Context: Resize/Move ...
# TRANSLATORS: Context: Add/Remove ...
# TRANSLATORS: Context: Undo/Redo ...
#: ../src/LevelEditor.cpp:1465 ../src/LevelEditor.cpp:1707
#: ../src/LevelEditor.cpp:1723 ../src/LevelEditor.cpp:1772
#: ../src/LevelEditor.cpp:4400
msgid "Custom scenery block"
msgstr ""
#: ../src/LevelEditor.cpp:1673
msgid "Toolbox"
msgstr ""
#: ../src/LevelEditor.cpp:1705
#, c-format
msgid "Resize %s"
msgstr ""
#: ../src/LevelEditor.cpp:1705
#, c-format
msgid "Move %s"
msgstr ""
# TRANSLATORS: Context: Undo/Redo ...
#: ../src/LevelEditor.cpp:1713
#, c-format
msgid "Move %d object"
msgid_plural "Move %d objects"
msgstr[0] ""
msgstr[1] ""
#: ../src/LevelEditor.cpp:1721
#, c-format
msgid "Add %s"
msgstr ""
#: ../src/LevelEditor.cpp:1721
#, c-format
msgid "Remove %s"
msgstr ""
# TRANSLATORS: Context: Undo/Redo ...
#: ../src/LevelEditor.cpp:1729
#, c-format
msgid "Add %d object"
msgid_plural "Add %d objects"
msgstr[0] ""
msgstr[1] ""
# TRANSLATORS: Context: Undo/Redo ...
#: ../src/LevelEditor.cpp:1731
#, c-format
msgid "Remove %d object"
msgid_plural "Remove %d objects"
msgstr[0] ""
msgstr[1] ""
# TRANSLATORS: Context: Undo/Redo ...
#: ../src/LevelEditor.cpp:1739
#, c-format
msgid "Add path to %s"
msgstr ""
# TRANSLATORS: Context: Undo/Redo ...
#: ../src/LevelEditor.cpp:1741
#, c-format
msgid "Remove a path point from %s"
msgstr ""
# TRANSLATORS: Context: Undo/Redo ...
#: ../src/LevelEditor.cpp:1747
#, c-format
msgid "Remove all paths from %s"
msgstr ""
# TRANSLATORS: Context: Undo/Redo ...
#: ../src/LevelEditor.cpp:1753
#, c-format
msgid "Add link from %s to %s"
msgstr ""
# TRANSLATORS: Context: Undo/Redo ...
#: ../src/LevelEditor.cpp:1759
#, c-format
msgid "Remove all links from %s"
msgstr ""
# TRANSLATORS: Context: Undo/Redo ...
#: ../src/LevelEditor.cpp:1766
msgid "Modify the %2 property of %1"
msgstr ""
# TRANSLATORS: Context: Undo/Redo ...
#: ../src/LevelEditor.cpp:1818
#, c-format
msgid "Edit the script of %s"
msgstr ""
# TRANSLATORS: Context: Undo/Redo ...
#: ../src/LevelEditor.cpp:1821
msgid "Edit the script of level"
msgstr ""
#: ../src/LevelEditor.cpp:2146 ../src/LevelEditor.cpp:2226
msgid "The level has unsaved changes."
msgstr ""
#: ../src/LevelEditor.cpp:2150 ../src/LevelEditor.cpp:2228
msgid "Are you sure you want to quit?"
msgstr ""
#: ../src/LevelEditor.cpp:2150 ../src/LevelEditor.cpp:2228
msgid "Quit prompt"
msgstr ""
#: ../src/LevelEditor.cpp:2797 ../src/LevelEditor.cpp:2799
#, c-format
msgid "Level \"%s\" saved"
msgstr ""
#: ../src/LevelEditor.cpp:2797 ../src/LevelEditor.cpp:2799
msgid "Saved"
msgstr ""
#: ../src/LevelEditor.cpp:2859 ../src/LevelEditSelect.cpp:208
msgid "Name:"
msgstr ""
#: ../src/LevelEditor.cpp:2866
msgid "Theme:"
msgstr ""
#: ../src/LevelEditor.cpp:2873
msgid "Examples: %DATA%/themes/classic"
msgstr ""
#: ../src/LevelEditor.cpp:2875
msgid "or %USER%/themes/Orange"
msgstr ""
#: ../src/LevelEditor.cpp:2878
msgid "Music:"
msgstr ""
#: ../src/LevelEditor.cpp:2887
msgid "Target time (s):"
msgstr ""
#: ../src/LevelEditor.cpp:2903
msgid "Target recordings:"
msgstr ""
#: ../src/LevelEditor.cpp:2919
msgid "Restart level editor is required"
msgstr ""
#: ../src/LevelEditor.cpp:3679 ../src/LevelEditor.cpp:3718
#: ../src/LevelEditor.cpp:3739
msgid "Please enter a layer name."
msgstr ""
#: ../src/LevelEditor.cpp:3679 ../src/LevelEditor.cpp:3683
#: ../src/LevelEditor.cpp:3718 ../src/LevelEditor.cpp:3722
#: ../src/LevelEditor.cpp:3739 ../src/LevelEditor.cpp:3743
#: ../src/LevelEditSelect.cpp:644 ../src/LevelEditSelect.cpp:683
#: ../src/LevelEditSelect.cpp:688 ../src/LevelEditSelect.cpp:693
#: ../src/LevelEditSelect.cpp:698 ../src/LevelEditSelect.cpp:796
msgid "Error"
msgstr ""
#: ../src/LevelEditor.cpp:3683 ../src/LevelEditor.cpp:3722
#, c-format
msgid "The layer '%s' already exists."
msgstr ""
#: ../src/LevelEditor.cpp:3743
msgid "Source and destination layers are the same."
msgstr ""
#: ../src/LevelEditor.cpp:3760
msgid "Scenery"
msgstr ""
#: ../src/LevelEditor.cpp:4190 ../src/LevelEditor.cpp:4218
#, c-format
msgid "Speed: %d = %0.2f block/s"
msgstr ""
#: ../src/LevelEditor.cpp:4203
msgid "Stop at this point"
msgstr ""
#: ../src/LevelEditor.cpp:4208
#, c-format
msgid "Pause: %d = %0.3fs"
msgstr ""
#: ../src/LevelEditSelect.cpp:41 ../src/TitleMenu.cpp:45
msgid "Map Editor"
msgstr ""
#: ../src/LevelEditSelect.cpp:66
msgid "New Levelpack"
msgstr ""
#: ../src/LevelEditSelect.cpp:71
msgid "Pack Properties"
msgstr ""
#: ../src/LevelEditSelect.cpp:76
msgid "Remove Pack"
msgstr ""
#: ../src/LevelEditSelect.cpp:81
msgid "Move Map"
msgstr ""
#: ../src/LevelEditSelect.cpp:89
msgid "Remove Map"
msgstr ""
#: ../src/LevelEditSelect.cpp:94
msgid "Edit Map"
msgstr ""
#: ../src/LevelEditSelect.cpp:205
msgid "Properties"
msgstr ""
#: ../src/LevelEditSelect.cpp:217
msgid "Description:"
msgstr ""
#: ../src/LevelEditSelect.cpp:226
msgid "Congratulation text:"
msgstr ""
#: ../src/LevelEditSelect.cpp:235
msgid "Music list:"
msgstr ""
#: ../src/LevelEditSelect.cpp:265 ../src/LevelEditSelect.cpp:485
msgid "Add level"
msgstr ""
#: ../src/LevelEditSelect.cpp:268
msgid "File name:"
msgstr ""
#: ../src/LevelEditSelect.cpp:293
msgid "Move level"
msgstr ""
#: ../src/LevelEditSelect.cpp:296
msgid "Level: "
msgstr ""
#: ../src/LevelEditSelect.cpp:310
msgid "Before"
msgstr ""
#: ../src/LevelEditSelect.cpp:311
msgid "After"
msgstr ""
#: ../src/LevelEditSelect.cpp:368 ../src/LevelPlaySelect.cpp:124
msgid "Individual levels which are not contained in any level packs"
msgstr ""
#: ../src/LevelEditSelect.cpp:577
#, c-format
msgid "Are you sure remove the level pack '%s'?"
msgstr ""
#: ../src/LevelEditSelect.cpp:577 ../src/LevelEditSelect.cpp:607
msgid "Remove prompt"
msgstr ""
#: ../src/LevelEditSelect.cpp:607
#, c-format
msgid "Are you sure remove the map '%s'?"
msgstr ""
#: ../src/LevelEditSelect.cpp:644
msgid "Levelpack name cannot be empty."
msgstr ""
#: ../src/LevelEditSelect.cpp:683
#, c-format
msgid "The levelpack directory '%s' already exists!"
msgstr ""
#: ../src/LevelEditSelect.cpp:688
#, c-format
msgid "Unable to create levelpack directory '%s'!"
msgstr ""
#: ../src/LevelEditSelect.cpp:693
#, c-format
msgid "The levelpack file '%s' already exists!"
msgstr ""
#: ../src/LevelEditSelect.cpp:698
#, c-format
msgid "Unable to create levelpack file '%s'!"
msgstr ""
#: ../src/LevelEditSelect.cpp:758
msgid "No file name given for the new level."
msgstr ""
#: ../src/LevelEditSelect.cpp:758
msgid "Missing file name"
msgstr ""
#: ../src/LevelEditSelect.cpp:796
#, c-format
msgid "The file %s already exists."
msgstr ""
#: ../src/LevelEditSelect.cpp:849
msgid "The entered level number isn't valid!"
msgstr ""
#: ../src/LevelEditSelect.cpp:849
msgid "Illegal number"
msgstr ""
#: ../src/LevelInfoRender.cpp:19
msgid "Choose a level"
msgstr ""
#: ../src/LevelInfoRender.cpp:20
msgid "Time:"
msgstr ""
#: ../src/LevelInfoRender.cpp:21 ../src/StatisticsScreen.cpp:259
msgid "Recordings:"
msgstr ""
#: ../src/LevelPackManager.cpp:124
msgid "Custom Levels"
msgstr ""
#: ../src/LevelPlaySelect.cpp:41
msgid "Select Level"
msgstr ""
# TRANSLATORS: Used for button which clear any level progress like unlocked levels and highscores.
#: ../src/OptionsMenu.cpp:66
msgid "Clear Progress"
msgstr ""
#: ../src/OptionsMenu.cpp:109
msgid "General"
msgstr ""
#: ../src/OptionsMenu.cpp:110
msgid "Controls"
msgstr ""
#: ../src/OptionsMenu.cpp:121
msgid "Music"
msgstr ""
#: ../src/OptionsMenu.cpp:129
msgid "Sound"
msgstr ""
#: ../src/OptionsMenu.cpp:137
msgid "Resolution"
msgstr ""
#: ../src/OptionsMenu.cpp:177
msgid "Language"
msgstr ""
# TRANSLATORS: as detect user's language automatically
#: ../src/OptionsMenu.cpp:185
msgid "Auto-Detect"
msgstr ""
#: ../src/OptionsMenu.cpp:209
msgid "Theme"
msgstr ""
#: ../src/OptionsMenu.cpp:247
msgid "Internet proxy"
msgstr ""
#: ../src/OptionsMenu.cpp:256
msgid "Fullscreen"
msgstr ""
#: ../src/OptionsMenu.cpp:261
msgid "Quick record"
msgstr ""
#: ../src/OptionsMenu.cpp:266
msgid "Internet"
msgstr ""
#: ../src/OptionsMenu.cpp:271
msgid "Fade transition"
msgstr ""
#: ../src/OptionsMenu.cpp:294
msgid "Save Changes"
msgstr ""
#: ../src/OptionsMenu.cpp:513
msgid "Do you really want to reset level progress?"
msgstr ""
#: ../src/OptionsMenu.cpp:513
msgid "Warning"
msgstr ""
#: ../src/StatisticsManager.cpp:386
msgid "New achievement:"
msgstr ""
#: ../src/StatisticsManager.cpp:394
#, c-format
msgid "Achieved on %s"
msgstr ""
#: ../src/StatisticsManager.cpp:400
msgid "Unknown achievement"
msgstr ""
#: ../src/StatisticsManager.cpp:406
#, c-format
msgid "Achieved %1.0f%%"
msgstr ""
#: ../src/StatisticsManager.cpp:410
msgid "Not achieved"
msgstr ""
#: ../src/StatisticsScreen.cpp:57 ../src/TitleMenu.cpp:55
msgid "Achievements and Statistics"
msgstr ""
#: ../src/StatisticsScreen.cpp:166
msgid "Achievements"
msgstr ""
#: ../src/StatisticsScreen.cpp:167
msgid "Statistics"
msgstr ""
#: ../src/StatisticsScreen.cpp:234
msgid "Total"
msgstr ""
#: ../src/StatisticsScreen.cpp:246
msgid "Traveling distance (m)"
msgstr ""
#: ../src/StatisticsScreen.cpp:247
msgid "Jump times"
msgstr ""
#: ../src/StatisticsScreen.cpp:248
msgid "Die times"
msgstr ""
#: ../src/StatisticsScreen.cpp:249
msgid "Squashed times"
msgstr ""
#: ../src/StatisticsScreen.cpp:260
msgid "Switch pulled times:"
msgstr ""
#: ../src/StatisticsScreen.cpp:261
msgid "Swap times:"
msgstr ""
#: ../src/StatisticsScreen.cpp:262
msgid "Save times:"
msgstr ""
#: ../src/StatisticsScreen.cpp:263
msgid "Load times:"
msgstr ""
#: ../src/StatisticsScreen.cpp:268
msgid "Completed levels:"
msgstr ""
#: ../src/StatisticsScreen.cpp:306
msgid "In-game time:"
msgstr ""
#: ../src/StatisticsScreen.cpp:308
msgid "Level editing time:"
msgstr ""
#: ../src/StatisticsScreen.cpp:310
msgid "Created levels:"
msgstr ""
#: ../src/TitleMenu.cpp:44
msgid "Options"
msgstr ""
#: ../src/TitleMenu.cpp:47
msgid "Quit"
msgstr ""
#: ../src/TitleMenu.cpp:131
msgid "Enable internet in order to install addons."
msgstr ""
#: ../src/TitleMenu.cpp:131
msgid "Internet disabled"
msgstr ""
# TRANSLATORS: name of a key
msgctxt "keys"
msgid "Return"
msgstr ""
# TRANSLATORS: name of a key
msgctxt "keys"
msgid "Escape"
msgstr ""
# TRANSLATORS: name of a key
msgctxt "keys"
msgid "Backspace"
msgstr ""
# TRANSLATORS: name of a key
msgctxt "keys"
msgid "Tab"
msgstr ""
# TRANSLATORS: name of a key
msgctxt "keys"
msgid "Space"
msgstr ""
# TRANSLATORS: name of a key
msgctxt "keys"
msgid "CapsLock"
msgstr ""
# TRANSLATORS: name of a key
msgctxt "keys"
msgid "PrintScreen"
msgstr ""
# TRANSLATORS: name of a key
msgctxt "keys"
msgid "ScrollLock"
msgstr ""
# TRANSLATORS: name of a key
msgctxt "keys"
msgid "Pause"
msgstr ""
# TRANSLATORS: name of a key
msgctxt "keys"
msgid "Insert"
msgstr ""
# TRANSLATORS: name of a key
msgctxt "keys"
msgid "Home"
msgstr ""
# TRANSLATORS: name of a key
msgctxt "keys"
msgid "PageUp"
msgstr ""
# TRANSLATORS: name of a key
msgctxt "keys"
msgid "Delete"
msgstr ""
# TRANSLATORS: name of a key
msgctxt "keys"
msgid "End"
msgstr ""
# TRANSLATORS: name of a key
msgctxt "keys"
msgid "PageDown"
msgstr ""
# TRANSLATORS: name of a key
msgctxt "keys"
msgid "Right"
msgstr ""
# TRANSLATORS: name of a key
msgctxt "keys"
msgid "Left"
msgstr ""
# TRANSLATORS: name of a key
msgctxt "keys"
msgid "Down"
msgstr ""
# TRANSLATORS: name of a key
msgctxt "keys"
msgid "Up"
msgstr ""
# TRANSLATORS: name of a key
msgctxt "keys"
msgid "Numlock"
msgstr ""
# TRANSLATORS: name of a key
msgctxt "keys"
msgid "SysReq"
msgstr ""
# TRANSLATORS: name of a key
msgctxt "keys"
msgid "Left Ctrl"
msgstr ""
# TRANSLATORS: name of a key
msgctxt "keys"
msgid "Left Shift"
msgstr ""
# TRANSLATORS: name of a key
msgctxt "keys"
msgid "Left Alt"
msgstr ""
# TRANSLATORS: name of a key
msgctxt "keys"
msgid "Left GUI"
msgstr ""
# TRANSLATORS: name of a key
msgctxt "keys"
msgid "Right Ctrl"
msgstr ""
# TRANSLATORS: name of a key
msgctxt "keys"
msgid "Right Shift"
msgstr ""
# TRANSLATORS: name of a key
msgctxt "keys"
msgid "Right Alt"
msgstr ""
# TRANSLATORS: name of a key
msgctxt "keys"
msgid "Right GUI"
msgstr ""
diff --git a/data/locale/ru.po b/data/locale/ru.po
index 27d0e48..8b01f69 100644
--- a/data/locale/ru.po
+++ b/data/locale/ru.po
@@ -1,2027 +1,2027 @@
# Russian translation for Me and My Shadow strings
# Copyright (C)
# This file is distributed under the same license as the meandmyshadow package.
# Jz Pan <acme_pjz@hotmail.com>, 2012.
#
msgid ""
msgstr ""
"Project-Id-Version: Russian (Me And My Shadow)\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-09-09 16:18+0800\n"
-"PO-Revision-Date: 2018-09-29 22:36+0000\n"
+"PO-Revision-Date: 2018-10-16 17:37+0000\n"
"Last-Translator: mesnevi <shams@airpost.net>\n"
"Language-Team: Russian <https://hosted.weblate.org/projects/me-and-my-shadow/"
"translations/ru/>\n"
"Language: ru\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<="
"4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
-"X-Generator: Weblate 3.2-dev\n"
+"X-Generator: Weblate 3.2.1\n"
#: ../src/AchievementList.cpp:43
msgid "Newbie"
msgstr "Новичок"
#: ../src/AchievementList.cpp:43
msgid "Complete a level."
msgstr "Завершить уровень."
#: ../src/AchievementList.cpp:44
msgid "Experienced player"
msgstr "Матёрый игрок"
#: ../src/AchievementList.cpp:44
msgid "Complete 50 levels."
msgstr "Завершить 50 уровней."
#: ../src/AchievementList.cpp:45
msgid "Good job!"
msgstr "Молодец!"
#: ../src/AchievementList.cpp:45
msgid "Receive a gold medal."
msgstr "Получить золотую медаль."
#: ../src/AchievementList.cpp:46
msgid "Expert"
msgstr "Специалист"
#: ../src/AchievementList.cpp:46
msgid "Earn 50 gold medal."
msgstr "Получить 50 золотых медалей."
#: ../src/AchievementList.cpp:48
msgid "Graduate"
msgstr "Выпускник"
#: ../src/AchievementList.cpp:48
msgid "Complete the tutorial level pack."
msgstr "Завершить все обучающие уровни."
#: ../src/AchievementList.cpp:49
msgid "Outstanding graduate"
msgstr "Отличник"
#: ../src/AchievementList.cpp:49
msgid "Complete the tutorial level pack with gold for all levels."
msgstr "Завершить все обучающие уровни, собрав все золотые медали."
#: ../src/AchievementList.cpp:51
msgid "Hooked"
msgstr "Зависимость"
#: ../src/AchievementList.cpp:51
msgid "Play Me and My Shadow for more than 2 hours."
msgstr "За игрой прошло более двух часов."
#: ../src/AchievementList.cpp:52
msgid "Loyal fan of Me and My Shadow"
msgstr "Преданный поклонник"
#: ../src/AchievementList.cpp:52
msgid "Play Me and My Shadow for more than 24 hours."
msgstr "За игрой прошло более суток."
#: ../src/AchievementList.cpp:54
msgid "Constructor"
msgstr "Конструктор"
#: ../src/AchievementList.cpp:54
msgid "Use the level editor for more than 2 hours."
msgstr "Использовать редактор уровней более двух часов."
#: ../src/AchievementList.cpp:55
msgid "The creator"
msgstr "Творец"
#: ../src/AchievementList.cpp:55
msgid "Use the level editor for more than 24 hours."
msgstr "Использовать редактор уровней более суток."
#: ../src/AchievementList.cpp:57
msgid "Look, cute level!"
msgstr "Ах, какая прелесть!"
#: ../src/AchievementList.cpp:57
msgid "Create a level for the first time."
msgstr "Создать свой первый уровень."
#: ../src/AchievementList.cpp:58
msgid "The level museum"
msgstr "Музей уровнестроения"
#: ../src/AchievementList.cpp:58
msgid "Create 50 levels."
msgstr "Создать 50 уровней."
#: ../src/AchievementList.cpp:60
msgid "Hello, World!"
msgstr "Hello, World!"
#: ../src/AchievementList.cpp:60
msgid "Write a script for the first time."
msgstr "Написать свой первый скрипт."
#: ../src/AchievementList.cpp:62
msgid "Frog"
msgstr "Лягушка-попрыгушка"
#: ../src/AchievementList.cpp:62
msgid "Jump 1000 times."
msgstr "Прыгнуть 1000 раз."
#: ../src/AchievementList.cpp:64
msgid "Wanderer"
msgstr "Пешеход"
#: ../src/AchievementList.cpp:64
msgid "Travel 100 meters."
msgstr "Пройти 100 метров."
#: ../src/AchievementList.cpp:65
msgid "Runner"
msgstr "Бродилка"
#: ../src/AchievementList.cpp:65
msgid "Travel 1 kilometer."
msgstr "Пройти 1 километр."
#: ../src/AchievementList.cpp:66
msgid "Long distance runner"
msgstr "Дальнобойщик"
#: ../src/AchievementList.cpp:66
msgid "Travel 10 kilometers."
msgstr "Пройти 10 километров."
#: ../src/AchievementList.cpp:67
msgid "Marathon runner"
msgstr "Марафонец"
#: ../src/AchievementList.cpp:67
msgid "Travel 42,195 meters."
msgstr "Пройти 42195 метров."
#: ../src/AchievementList.cpp:69
msgid "Be careful!"
msgstr "Поберегись!"
#: ../src/AchievementList.cpp:69
msgid "Die for the first time."
msgstr "Умереть впервые."
#: ../src/AchievementList.cpp:70
msgid "It doesn't matter..."
msgstr "Ну и ладно..."
#: ../src/AchievementList.cpp:70
msgid "Die 50 times."
msgstr "Умереть 50 раз."
#: ../src/AchievementList.cpp:71
msgid "Expert of trial and error"
msgstr "Лауреат премии Дарвина"
#: ../src/AchievementList.cpp:71
msgid "Die 1000 times."
msgstr "Умереть 1000 раз."
#: ../src/AchievementList.cpp:73
msgid "Keep an eye for moving blocks!"
msgstr "Осторожно, они двигаются!"
#: ../src/AchievementList.cpp:73
msgid "Get squashed for the first time."
msgstr "Быть раздавленным первый раз."
#: ../src/AchievementList.cpp:74
msgid "Potato masher"
msgstr "Пюре"
#: ../src/AchievementList.cpp:74
msgid "Get squashed 50 times."
msgstr "Быть раздавленным 50 раз."
#: ../src/AchievementList.cpp:76
msgid "Double kill"
msgstr "За двумя зайцами"
#: ../src/AchievementList.cpp:76
msgid "Get both the player and the shadow dead."
msgstr "Добиться смерти игрока и его тени."
#: ../src/AchievementList.cpp:78
msgid "Bad luck"
msgstr "Тотальное невезение"
#: ../src/AchievementList.cpp:78
msgid "Die 5 times in under 5 seconds."
msgstr "Умереть 5 раз за менее, чем 5 секунд."
#: ../src/AchievementList.cpp:79
msgid "This level is too dangerous"
msgstr "Да что ж это за жизнь такая?"
#: ../src/AchievementList.cpp:79
msgid "Die 10 times in under 5 seconds."
msgstr "Умереть 10 раз за менее, чем 5 секунд."
#: ../src/AchievementList.cpp:81
msgid "You forgot your friend"
msgstr "Друзья? Нет, не слышал"
#: ../src/AchievementList.cpp:81
msgid "Finish the level with the player or the shadow dead."
msgstr "Завершить уровень при том, что игрок или тень мертвы."
#: ../src/AchievementList.cpp:82
msgid "Just in time"
msgstr "Точность - вежливость королей"
#: ../src/AchievementList.cpp:82
msgid "Reach the exit with the player and the shadow simultaneously."
msgstr "Достичь выхода игроком и тенью одновременно."
#: ../src/AchievementList.cpp:84
msgid "Recorder"
-msgstr "По-медленнее, я записываю!"
+msgstr "Помедленнее, я записываю!"
#: ../src/AchievementList.cpp:84
msgid "Record 100 times."
msgstr "Выполнить запись 100 раз."
#: ../src/AchievementList.cpp:85
msgid "Shadowmaster"
msgstr "У меня все ходы записаны!"
#: ../src/AchievementList.cpp:85
msgid "Record 1000 times."
msgstr "Выполнить запись 1000 раз."
#: ../src/AchievementList.cpp:87
msgid "Switch puller"
msgstr "Выключателевключатель"
#: ../src/AchievementList.cpp:87
msgid "Pull the switch 100 times."
msgstr "Включить выключатель 100 раз."
#: ../src/AchievementList.cpp:88
msgid "The switch is broken!"
msgstr "Выключателеломатель"
#: ../src/AchievementList.cpp:88
msgid "Pull the switch 1000 times."
msgstr "Включить выключатель 1000 раз."
#: ../src/AchievementList.cpp:90
msgid "Swapper"
msgstr "Обменный пункт"
#: ../src/AchievementList.cpp:90
msgid "Swap 100 times."
msgstr "Поменяться 100 раз."
#: ../src/AchievementList.cpp:91
msgid "Player to shadow to player to shadow..."
-msgstr "Туда-сюдашка"
+msgstr "Туда-сюда-обратно"
#: ../src/AchievementList.cpp:91
msgid "Swap 1000 times."
msgstr "Поменяться 1000 раз."
#: ../src/AchievementList.cpp:93
msgid "Play it save"
msgstr "Хорошо сохранился"
#: ../src/AchievementList.cpp:93
msgid "Save 1000 times."
msgstr "Сохраниться 1000 раз."
#: ../src/AchievementList.cpp:94
msgid "This game is too hard"
msgstr "Сизифов труд"
#: ../src/AchievementList.cpp:94
msgid "Load the game 1000 times."
msgstr "Загрузить игру 1000 раз."
#: ../src/AchievementList.cpp:96
msgid "No, thanks"
msgstr "Нет уж, спасибо"
#: ../src/AchievementList.cpp:96
msgid "Complete a level with checkpoint, but without saving."
msgstr "Пройти уровень с чекпоинтом, но без сохранений."
#: ../src/AchievementList.cpp:98
msgid "Panic save"
msgstr "Паническое сохранение"
#: ../src/AchievementList.cpp:98
msgid "Save twice in 1 second."
msgstr "Сохраниться дважды за секунду."
#: ../src/AchievementList.cpp:99
msgid "Panic load"
msgstr "Паническая загрузка"
#: ../src/AchievementList.cpp:99
msgid "Load twice in 1 second."
msgstr "Загрузиться дважды за секунду."
#: ../src/AchievementList.cpp:101
msgid "Bad saving position"
msgstr "Местечко так себе"
#: ../src/AchievementList.cpp:101
msgid "Load the game and die within 1 second."
msgstr "Загрузить сохранённую игру и умереть в течении секунды."
#: ../src/AchievementList.cpp:102
msgid "This level is too hard"
msgstr "Да что ж такое"
#: ../src/AchievementList.cpp:102
msgid "Load the same save and die 100 times."
msgstr "Загрузить игру и умереть 100 раз."
#: ../src/AchievementList.cpp:104
msgid "Quick swap"
msgstr "Быстрая смена"
#: ../src/AchievementList.cpp:104
msgid "Swap twice in under a second."
msgstr "Поменяться дважды за секунду."
#: ../src/AchievementList.cpp:107
msgid "Horizontal confusion"
msgstr "Сено-солома"
#: ../src/AchievementList.cpp:107
msgid "Press left and right simultaneously."
msgstr "Нажать влево и вправо одновременно."
#: ../src/AchievementList.cpp:109
msgid "Cheater"
msgstr "Жулик"
#: ../src/AchievementList.cpp:109
msgid "Cheat in game."
msgstr "Не надо было жульничать."
#: ../src/AchievementList.cpp:111
msgid "Programmer"
msgstr "Тыжпрограммист"
#: ../src/AchievementList.cpp:111
msgid "Play the development version of Me and My Shadow."
msgstr "Играть в версию в разработке."
#: ../src/Addons.cpp:44 ../src/LevelPackManager.cpp:108
msgid "Levels"
msgstr "Уровни"
#: ../src/Addons.cpp:44
msgid "Single level which usually contain demanding puzzles"
msgstr "Один уровень, который обычно содержит головоломки"
#: ../src/Addons.cpp:45
msgid "Levelpacks"
msgstr "Сборники уровней"
#: ../src/Addons.cpp:45
msgid "Collection of levels with the same author or style"
msgstr "Несколько уровней, созданных единым автором или в подобном стиле"
#: ../src/Addons.cpp:46
msgid "Themes"
msgstr "Темы"
#: ../src/Addons.cpp:46
msgid "Give every block and background a new look and feel"
msgstr "Придать каждому блоку и фону новый вид и стиль"
#: ../src/Addons.cpp:55 ../src/TitleMenu.cpp:46
msgid "Addons"
msgstr "Дополнения"
#: ../src/Addons.cpp:87
msgid "Unable to initialize addon menu:"
msgstr "Не удалось запустить меню дополнений:"
#: ../src/Addons.cpp:95 ../src/Addons.cpp:158 ../src/Addons.cpp:662
#: ../src/Addons.cpp:690 ../src/CreditsMenu.cpp:89 ../src/LevelSelect.cpp:168
#: ../src/StatisticsScreen.cpp:159
msgid "Back"
msgstr "Назад"
#: ../src/Addons.cpp:169
msgid "ERROR: unable to download addons file!"
msgstr "ОШИБКА: не удалось загрузить файл дополнения!"
#: ../src/Addons.cpp:182
msgid "ERROR: unable to load addon_list file!"
msgstr "ОШИБКА: не удалось загрузить файл addon_list!"
#: ../src/Addons.cpp:193
msgid "ERROR: Invalid file format of addons file!"
msgstr "ОШИБКА: Неверный формат файла дополнения!"
#: ../src/Addons.cpp:205
msgid "ERROR: Addon list version is unsupported!"
msgstr "ОШИБКА: версия списка дополнений не поддерживается!"
#: ../src/Addons.cpp:226
msgid "ERROR: Unable to create the installed_addons file."
msgstr "ОШИБКА: не удалось создать список дополнений installed_addons."
#: ../src/Addons.cpp:238
msgid "ERROR: Invalid file format of the installed_addons!"
msgstr "ОШИБКА: у списка дополнений installed_addons неправильный формат!"
#: ../src/Addons.cpp:389 ../src/Addons.cpp:621
#, c-format
msgid "by %s"
-msgstr "автор %"
+msgstr "автор: %s"
#: ../src/Addons.cpp:397
msgid "Installed"
msgstr "Установлено"
#: ../src/Addons.cpp:402
msgid "Updatable"
msgstr "Есть обновления"
#: ../src/Addons.cpp:412
msgid "Not installed"
msgstr "Не установлено"
#: ../src/Addons.cpp:625
#, c-format
msgid "Version: %d\n"
msgstr "Версия: %d\n"
#: ../src/Addons.cpp:627
#, c-format
msgid "Installed version: %d\n"
msgstr "Установленная версия: %d\n"
#: ../src/Addons.cpp:630
#, c-format
msgid "License: %s\n"
msgstr "Лицензия: %s\n"
#: ../src/Addons.cpp:633
#, c-format
msgid "Website: %s\n"
msgstr "Веб-сайт: %s\n"
#: ../src/Addons.cpp:637
msgid "(No descriptions provided)"
msgstr "(Нет описания)"
#: ../src/Addons.cpp:657 ../src/Addons.cpp:684
msgid "Remove"
msgstr "Удалить"
#: ../src/Addons.cpp:673
msgid "Update"
msgstr "Обновить"
#: ../src/Addons.cpp:679
msgid "Install"
msgstr "Установить"
#: ../src/Addons.cpp:774
#, c-format
msgid "This addon can't be removed because it's needed by %s."
msgstr "Это дополнение нельзя удалить, оно необходимо для правильной работы %s."
#: ../src/Addons.cpp:774 ../src/Addons.cpp:1051
msgid "Dependency"
msgstr "Зависимости"
#: ../src/Addons.cpp:803
#, c-format
msgid "WARNING: File '%s' appears to have been removed already."
msgstr "ВНИМАНИЕ: файл '%s' уже удалён."
#: ../src/Addons.cpp:803 ../src/Addons.cpp:810 ../src/Addons.cpp:818
#: ../src/Addons.cpp:825 ../src/Addons.cpp:834 ../src/Addons.cpp:840
#: ../src/Addons.cpp:859 ../src/Addons.cpp:866 ../src/Addons.cpp:893
#: ../src/Addons.cpp:900 ../src/Addons.cpp:907 ../src/Addons.cpp:918
#: ../src/Addons.cpp:947 ../src/Addons.cpp:952 ../src/Addons.cpp:962
#: ../src/Addons.cpp:968 ../src/Addons.cpp:981 ../src/Addons.cpp:986
#: ../src/Addons.cpp:1008 ../src/Addons.cpp:1014 ../src/Addons.cpp:1044
msgid "Addon error"
msgstr "Ошибка дополнения"
#: ../src/Addons.cpp:810
#, c-format
msgid "ERROR: Unable to remove file '%s'!"
msgstr "ОШИБКА: не получается удалить файл '%s'!"
#: ../src/Addons.cpp:818
#, c-format
msgid "WARNING: Directory '%s' appears to have been removed already."
msgstr "ВНИМАНИЕ: папка '%s' уже удалена."
#: ../src/Addons.cpp:825
#, c-format
msgid "ERROR: Unable to remove directory '%s'!"
msgstr "ОШИБКА: не получается удалить папку '%s'!"
#: ../src/Addons.cpp:834
#, c-format
msgid "WARNING: Level '%s' appears to have been removed already."
msgstr "ВНИМАНИЕ: уровень '%s' уже удалён."
#: ../src/Addons.cpp:840
#, c-format
msgid "ERROR: Unable to remove level '%s'!"
msgstr "ОШИБКА: не получается удалить уровень '%s'!"
#: ../src/Addons.cpp:859
#, c-format
msgid "WARNING: Levelpack directory '%s' appears to have been removed already."
msgstr "ВНИМАНИЕ: папка со сборником уровней '%s' уже удалена."
#: ../src/Addons.cpp:866
#, c-format
msgid "ERROR: Unable to remove levelpack directory '%s'!"
msgstr "ОШИБКА: не получается удалить набор уровней '%s'!"
#: ../src/Addons.cpp:893
#, c-format
msgid "ERROR: Unable to download addon file %s."
msgstr "ОШИБКА: не получается загрузить файл дополнения %s."
#: ../src/Addons.cpp:900
#, c-format
msgid "ERROR: Unable to extract addon file %s."
msgstr "ОШИБКА: не получается распаковать файл дополнения %s."
#: ../src/Addons.cpp:907
msgid "ERROR: Addon is missing metadata!"
msgstr "ОШИБКА: в дополнении отсутствуют метаданные!"
#: ../src/Addons.cpp:918
msgid "ERROR: Invalid file format for metadata file!"
msgstr "ОШИБКА: неверный формат файла метаданных!"
#: ../src/Addons.cpp:947
#, c-format
msgid "WARNING: File '%s' already exists, addon may be broken or not working!"
msgstr "ВНИМАНИЕ: файл '%s' уже существует, дополнение может глючить или не работать!"
#: ../src/Addons.cpp:952
#, c-format
msgid ""
"WARNING: Unable to copy file '%s' to '%s', addon may be broken or not "
"working!"
msgstr ""
"ВНИМАНИЕ: не получается скопировать файл '%s' в '%s', "
"дополнение может глючить или не работать!"
#: ../src/Addons.cpp:962
#, c-format
msgid ""
"WARNING: Destination directory '%s' already exists, addon may be broken or "
"not working!"
msgstr ""
"ВНИМАНИЕ: папка '%s' уже существует, "
"дополнение может глючить или не работать!"
#: ../src/Addons.cpp:968 ../src/Addons.cpp:1014
#, c-format
msgid ""
"WARNING: Unable to move directory '%s' to '%s', addon may be broken or not "
"working!"
msgstr ""
"ВНИМАНИЕ: не получается переместить папку '%s' в '%s', "
"дополнение может глючить или не работать!"
#: ../src/Addons.cpp:981
#, c-format
msgid "WARNING: Level '%s' already exists, addon may be broken or not working!"
msgstr "ВНИМАНИЕ: уровень '%s' уже существует, дополнение может глючить или не работать!"
#: ../src/Addons.cpp:986
#, c-format
msgid ""
"WARNING: Unable to copy level '%s' to '%s', addon may be broken or not "
"working!"
msgstr ""
"ВНИМАНИЕ: не получается скопировать уровень '%s' в '%s', "
"дополнение может глючить или не работать!"
#: ../src/Addons.cpp:1008
#, c-format
msgid ""
"WARNING: Levelpack directory '%s' already exists, addon may be broken or not "
"working!"
msgstr ""
"ВНИМАНИЕ: папка со сборником уровней '%s' уже существует, "
"дополнение может глючить или не работать!"
#: ../src/Addons.cpp:1044
#, c-format
msgid "ERROR: Addon requires another addon (%s) which can't be found!"
msgstr ""
"ОШИБКА: для правильной работы дополнения необходимо установить другое "
"дополнение (%s), однако его не удалось найти!"
#: ../src/Addons.cpp:1051
#, c-format
msgid "The addon %s is needed and will be installed now."
msgstr "Необходимо установить дополнение %s. Установка сейчас начнётся."
#: ../src/Block.cpp:822 ../src/LevelEditor.cpp:265
msgid "On"
msgstr "Вкл."
#: ../src/Block.cpp:823 ../src/LevelEditor.cpp:266
msgid "Off"
msgstr "Выкл."
#: ../src/CommandManager.cpp:41
#, c-format
msgid "Undo %s"
msgstr "Отменить %s"
#: ../src/CommandManager.cpp:43
msgid "Can't undo"
msgstr "Невозможно отменить"
#: ../src/CommandManager.cpp:49
#, c-format
msgid "Redo %s"
msgstr "Повторить %s"
#: ../src/CommandManager.cpp:51
msgid "Can't redo"
msgstr "Невозможно повторить"
#: ../src/Commands.cpp:190
msgid "Resize level"
msgstr "Изменить размер уровня"
#: ../src/Commands.cpp:807
msgid "Modify level property"
msgstr "Изменить свойства уровня"
#: ../src/Commands.cpp:919
#, c-format
msgid "Add scenery layer %s"
msgstr "Добавить фоновый слой %s"
#: ../src/Commands.cpp:921
#, c-format
msgid "Delete scenery layer %s"
msgstr "Удалить фоновый слой %s"
#: ../src/Commands.cpp:951
#, c-format
msgid "Modify the property of scenery layer %s"
msgstr "Изменить свойства фонового слоя %s"
#: ../src/Commands.cpp:1040
#, c-format
msgid "Move %d object from layer %s to layer %s"
msgid_plural "Move %d objects from layer %s to layer %s"
msgstr[0] "Переместить %d объект из слоя %s в слой %s"
msgstr[1] "Переместить %d объекта из слоя %s в слой %s"
msgstr[2] "Переместить %d объектов из слоя %s в слой %s"
msgstr[3] "Переместить %d объектов из слоя %s в слой %s"
#: ../src/CreditsMenu.cpp:35 ../src/TitleMenu.cpp:53
msgid "Credits"
msgstr "Благодарности"
# TRANSLATORS: Font used in GUI:
# - Use "knewave" for languages using Latin and Latin-derived alphabets
# - "DroidSansFallback" can be used for non-Latin writing systems
#: ../src/Functions.cpp:569 ../src/Functions.cpp:570 ../src/Functions.cpp:571
#: ../src/Functions.cpp:588
msgid "knewave"
msgstr "DejaVuSansCondensed-Oblique"
#: ../src/Functions.cpp:575
msgid "Blokletters-Viltstift"
msgstr "DejaVuSansCondensed-Oblique"
#: ../src/Functions.cpp:674
msgid "Loading..."
msgstr "Загрузка..."
#: ../src/Functions.cpp:1243 ../src/Functions.cpp:1270
#: ../src/LevelEditor.cpp:559 ../src/LevelEditor.cpp:693
#: ../src/LevelEditor.cpp:758 ../src/LevelEditor.cpp:821
#: ../src/LevelEditor.cpp:908 ../src/LevelEditor.cpp:1033
#: ../src/LevelEditor.cpp:1083 ../src/LevelEditor.cpp:1180
#: ../src/LevelEditor.cpp:1244 ../src/LevelEditor.cpp:2923
#: ../src/LevelEditSelect.cpp:244 ../src/LevelEditSelect.cpp:277
#: ../src/LevelEditSelect.cpp:317
msgid "OK"
msgstr "Да"
#: ../src/Functions.cpp:1244 ../src/Functions.cpp:1256
#: ../src/Functions.cpp:1266 ../src/LevelEditor.cpp:565
#: ../src/LevelEditor.cpp:699 ../src/LevelEditor.cpp:764
#: ../src/LevelEditor.cpp:827 ../src/LevelEditor.cpp:914
#: ../src/LevelEditor.cpp:1039 ../src/LevelEditor.cpp:1089
#: ../src/LevelEditor.cpp:1186 ../src/LevelEditor.cpp:1250
#: ../src/LevelEditor.cpp:2929 ../src/LevelEditSelect.cpp:248
#: ../src/LevelEditSelect.cpp:281 ../src/LevelEditSelect.cpp:321
#: ../src/OptionsMenu.cpp:289
msgid "Cancel"
msgstr "Отмена"
#: ../src/Functions.cpp:1248
msgid "Abort"
msgstr "Прервать"
#: ../src/Functions.cpp:1249 ../src/Functions.cpp:1265
msgid "Retry"
msgstr "Попробовать снова"
#: ../src/Functions.cpp:1250
msgid "Ignore"
msgstr "Пропустить"
#: ../src/Functions.cpp:1254 ../src/Functions.cpp:1260
msgid "Yes"
msgstr "Да"
#: ../src/Functions.cpp:1255 ../src/Functions.cpp:1261
msgid "No"
msgstr "Нет"
#: ../src/Game.cpp:280 ../src/Game.cpp:1236
#, c-format
msgid "Level %d %s"
msgstr "Уровень %d %s"
#: ../src/Game.cpp:915
#, c-format
msgid "Press %s key to save the game."
msgstr "Чтобы сохранить игру, нажмите клавишу %s."
#: ../src/Game.cpp:920
#, c-format
msgid "Press %s key to swap the position of player and shadow."
msgstr "Чтобы поменять позицию игрока и тени, нажмите клавишу %s."
#: ../src/Game.cpp:925
#, c-format
msgid "Press %s key to activate the switch."
msgstr "Чтобы активировать переключатель, нажмите клавишу %s."
#: ../src/Game.cpp:930
#, c-format
msgid "Press %s key to teleport."
msgstr "Чтобы телепортироваться, нажмите клавишу %s."
#: ../src/Game.cpp:972
#, c-format
msgid "Press %s to restart current level or press %s to load the game."
msgstr "Чтобы перезапустить этот уровень, нажмите %s, или нажмите %s, чтобы загрузить игру."
#: ../src/Game.cpp:983
#, c-format
msgid "Press %s to restart current level."
msgstr "Чтобы перезапустить этот уровень, нажмите %s."
#: ../src/Game.cpp:996
msgid "Your shadow has died."
msgstr "Ваша тень умерла."
#: ../src/Game.cpp:1052
#, c-format
msgid "%d recording"
msgid_plural "%d recordings"
msgstr[0] "%d запись"
msgstr[1] "%d записи"
msgstr[2] "%d записей"
msgstr[3] "%d записей"
#: ../src/Game.cpp:1224
msgid "You've finished:"
msgstr "Завершённых уровней:"
#: ../src/Game.cpp:1291
#, c-format
msgid "Time: %-.2fs"
msgstr "Время: %-.2fs"
#: ../src/Game.cpp:1300
#, c-format
msgid "Best time: %-.2fs"
msgstr "Лучшее время: %-.2fs"
#: ../src/Game.cpp:1311
#, c-format
msgid "Target time: %-.2fs"
msgstr "Необходимое время: %-.2fs"
#: ../src/Game.cpp:1332
#, c-format
msgid "Recordings: %d"
msgstr "Записей: %d"
#: ../src/Game.cpp:1340
#, c-format
msgid "Best recordings: %d"
msgstr "Лучшая запись: %d"
#: ../src/Game.cpp:1350
#, c-format
msgid "Target recordings: %d"
msgstr "Необходимо записей: %d"
#: ../src/Game.cpp:1363
#, c-format
msgid "You earned the %s medal"
msgstr "Вы получили %s медаль"
#: ../src/Game.cpp:1363
msgid "GOLD"
msgstr "ЗОЛОТУЮ"
#: ../src/Game.cpp:1363
msgid "SILVER"
msgstr "СЕРЕБРЯНУЮ"
#: ../src/Game.cpp:1363
msgid "BRONZE"
msgstr "БРОНЗОВУЮ"
#: ../src/Game.cpp:1390
msgid "Menu"
msgstr "Меню"
#: ../src/Game.cpp:1397 ../src/InputManager.cpp:47
msgid "Restart"
msgstr "Снова"
#: ../src/Game.cpp:1404
msgid "Next"
msgstr "Далее"
#: ../src/Game.cpp:1430
msgid "Game replay is done."
msgstr "Воспроизведение завершено."
#: ../src/Game.cpp:1430
msgid "Game Replay"
msgstr "Воспроизведение игры"
#: ../src/Game.cpp:1767 ../src/Game.cpp:1769
msgid "Congratulations"
msgstr "Поздравляем"
#: ../src/Game.cpp:1769
msgid "You have finished the levelpack!"
msgstr "Вы закончили этот сборник уровней!"
#: ../src/InputManager.cpp:46
msgid "Up (in menu)"
msgstr "Вверх (в меню)"
#: ../src/InputManager.cpp:46
msgid "Down (in menu)"
msgstr "Вниз (в меню)"
#: ../src/InputManager.cpp:46
msgid "Left"
msgstr "Влево"
#: ../src/InputManager.cpp:46
msgid "Right"
msgstr "Вправо"
#: ../src/InputManager.cpp:46
msgid "Jump"
msgstr "Прыжок"
#: ../src/InputManager.cpp:46
msgid "Action"
msgstr "Действие"
#: ../src/InputManager.cpp:46
msgid "Space (Record)"
msgstr "Пробел (Запись)"
#: ../src/InputManager.cpp:46
msgid "Cancel recording"
msgstr "Отменить запись"
#: ../src/InputManager.cpp:47
msgid "Escape"
msgstr "Выход"
#: ../src/InputManager.cpp:47
msgid "Tab (View shadow/Level prop.)"
msgstr "Tab (См. тень/Настр. уровня)"
#: ../src/InputManager.cpp:47
msgid "Save game (in editor)"
msgstr "Сохранить игру (в редакторе)"
#: ../src/InputManager.cpp:47
msgid "Load game"
msgstr "Загрузить игру"
#: ../src/InputManager.cpp:47
msgid "Swap (in editor)"
msgstr "Поменяться местами (в редакторе)"
#: ../src/InputManager.cpp:48
msgid "Teleport (in editor)"
msgstr "Телепорт (в редакторе)"
#: ../src/InputManager.cpp:48
msgid "Suicide (in editor)"
msgstr "Смерть (в редакторе)"
#: ../src/InputManager.cpp:48
msgid "Shift (in editor)"
msgstr "Сдвиг (в редакторе)"
#: ../src/InputManager.cpp:48
msgid "Next block type (in Editor)"
msgstr "Следующий вид блока (в редакторе)"
#: ../src/InputManager.cpp:49
msgid "Previous block type (in editor)"
msgstr "Предыдущий вид блока (в редакторе)"
#: ../src/InputManager.cpp:49
msgid "Select (in menu)"
msgstr "Выбрать (в меню)"
#: ../src/InputManager.cpp:156
#, c-format
msgid "(Key %d)"
msgstr "(Клавиша %d)"
#: ../src/InputManager.cpp:163
#, c-format
msgid "Joystick axis %d %s"
msgstr "Ось джойстика %d %s"
#: ../src/InputManager.cpp:166
#, c-format
msgid "Joystick button %d"
msgstr "Кнопка джойстика %d"
#: ../src/InputManager.cpp:171
#, c-format
msgid "Joystick hat %d left"
msgstr "Джойстик %d влево"
#: ../src/InputManager.cpp:174
#, c-format
msgid "Joystick hat %d right"
msgstr "Джойстик %d вправо"
#: ../src/InputManager.cpp:177
#, c-format
msgid "Joystick hat %d up"
msgstr "Джойстик %d вверх"
#: ../src/InputManager.cpp:180
#, c-format
msgid "Joystick hat %d down"
msgstr "Джойстик %d вниз"
#: ../src/InputManager.cpp:185
#, c-format
msgid "Joystick hat %d %d"
msgstr "Джойстик %d %d"
#: ../src/InputManager.cpp:202
msgid "OR"
msgstr "ИЛИ"
#: ../src/InputManager.cpp:416
msgid "Select an item and press a key to change it."
msgstr "Выберите элемент и нажмите клавишу, чтобы изменить его."
#: ../src/InputManager.cpp:419
msgid "Press backspace to clear the selected item."
msgstr "Нажмите backspace, чтобы очистить выбранный элемент."
#: ../src/LevelEditor.cpp:56
msgid "Block"
msgstr "Блок"
#: ../src/LevelEditor.cpp:56
msgid "Player Start"
msgstr "Здесь начинает игрок"
#: ../src/LevelEditor.cpp:56
msgid "Shadow Start"
msgstr "Здесь начинает тень"
#: ../src/LevelEditor.cpp:57
msgid "Exit"
msgstr "Выход"
#: ../src/LevelEditor.cpp:57
msgid "Shadow Block"
msgstr "Блок для тени"
#: ../src/LevelEditor.cpp:57
msgid "Spikes"
msgstr "Шипы"
#: ../src/LevelEditor.cpp:58
msgid "Checkpoint"
msgstr "Чекпоинт"
#: ../src/LevelEditor.cpp:58 ../src/LevelEditSelect.cpp:312
msgid "Swap"
msgstr "Поменяться"
#: ../src/LevelEditor.cpp:58
msgid "Fragile"
msgstr "Хрупкий"
#: ../src/LevelEditor.cpp:59
msgid "Moving Block"
msgstr "Движущийся блок"
#: ../src/LevelEditor.cpp:59
msgid "Moving Shadow Block"
msgstr "Движущийся блок для тени"
#: ../src/LevelEditor.cpp:59
msgid "Moving Spikes"
msgstr "Движущиеся шипы"
#: ../src/LevelEditor.cpp:60
msgid "Teleporter"
msgstr "Телепортер"
#: ../src/LevelEditor.cpp:60
msgid "Button"
msgstr "Кнопка"
#: ../src/LevelEditor.cpp:60
msgid "Switch"
msgstr "Переключатель"
#: ../src/LevelEditor.cpp:61
msgid "Conveyor Belt"
msgstr "Конвейер"
#: ../src/LevelEditor.cpp:61
msgid "Shadow Conveyor Belt"
msgstr "Конвейер для тени"
#: ../src/LevelEditor.cpp:61
msgid "Notification Block"
msgstr "Блок оповещения"
#: ../src/LevelEditor.cpp:61
msgid "Collectable"
msgstr "Подбираемый предмет"
#: ../src/LevelEditor.cpp:61
msgid "Pushable"
msgstr "Толкаемый предмет"
#: ../src/LevelEditor.cpp:65 ../src/LevelEditor.cpp:310
msgid "Select"
msgstr "Выбрать"
#: ../src/LevelEditor.cpp:65
msgid "Add"
msgstr "Добавить"
#: ../src/LevelEditor.cpp:65 ../src/LevelEditor.cpp:311
msgid "Delete"
msgstr "Удалить"
#: ../src/LevelEditor.cpp:65 ../src/LevelPlaySelect.cpp:66
#: ../src/TitleMenu.cpp:43
msgid "Play"
msgstr "Играть"
#: ../src/LevelEditor.cpp:65 ../src/LevelEditor.cpp:2852
msgid "Level settings"
msgstr "Настройки уровня"
#: ../src/LevelEditor.cpp:65
msgid "Save level"
msgstr "Сохранить уровень"
#: ../src/LevelEditor.cpp:65
msgid "Back to menu"
msgstr "Назад в меню"
#: ../src/LevelEditor.cpp:65
msgid "Configure"
msgstr "Настройки"
#: ../src/LevelEditor.cpp:84
#, c-format
msgid "%s (Scenery)"
msgstr "%s (Фон)"
#: ../src/LevelEditor.cpp:267
msgid "Toggle"
msgstr "Переключать"
#: ../src/LevelEditor.cpp:270
msgid "Complete"
msgstr "Завершённый"
#: ../src/LevelEditor.cpp:271
msgid "One step"
msgstr "Один шаг"
#: ../src/LevelEditor.cpp:272
msgid "Two steps"
msgstr "Два шага"
#: ../src/LevelEditor.cpp:273
msgid "Gone"
msgstr "Разрушен"
#: ../src/LevelEditor.cpp:291
msgid "Negative infinity"
msgstr "Минус бесконечность"
#: ../src/LevelEditor.cpp:293
msgid "Zero"
msgstr "Ноль"
#: ../src/LevelEditor.cpp:295
msgid "Level size"
msgstr "Размер уровня"
#: ../src/LevelEditor.cpp:297
msgid "Positive infinity"
msgstr "Плюс бесконечность"
#: ../src/LevelEditor.cpp:299
msgid "Default"
msgstr "Стандартный"
#: ../src/LevelEditor.cpp:308
msgid "Deselect"
msgstr "Отменить выбор"
#: ../src/LevelEditor.cpp:318 ../src/LevelEditor.cpp:1136
#, c-format
msgid "Horizontal repeat start: %s"
msgstr "Начать горизонтальное повторение: %s"
#: ../src/LevelEditor.cpp:320 ../src/LevelEditor.cpp:1137
#, c-format
msgid "Horizontal repeat end: %s"
msgstr "Завершить горизонтальное повторение: %s"
#: ../src/LevelEditor.cpp:322 ../src/LevelEditor.cpp:1138
#, c-format
msgid "Vertical repeat start: %s"
msgstr "Начать вертикальное повторение: %s"
#: ../src/LevelEditor.cpp:324 ../src/LevelEditor.cpp:1139
#, c-format
msgid "Vertical repeat end: %s"
msgstr "Завершить вертикальное повторение: %s"
#: ../src/LevelEditor.cpp:329 ../src/LevelEditor.cpp:1150
msgid "Custom scenery"
msgstr "Настроить фон"
#: ../src/LevelEditor.cpp:335 ../src/LevelEditor.cpp:600
#: ../src/LevelEditor.cpp:602
msgid "Visible"
msgstr "Видимый"
#: ../src/LevelEditor.cpp:344
msgid "Link"
msgstr "Ссылка"
#: ../src/LevelEditor.cpp:345
msgid "Remove Links"
msgstr "Убрать ссылки"
#: ../src/LevelEditor.cpp:349 ../src/LevelEditor.cpp:624
#: ../src/LevelEditor.cpp:626
msgid "Automatic"
msgstr "Авто"
#: ../src/LevelEditor.cpp:359 ../src/LevelEditor.cpp:649
#, c-format
msgid "Behavior: %s"
msgstr "Поведение: %s"
#: ../src/LevelEditor.cpp:362
msgid "Path"
msgstr "Путь"
#: ../src/LevelEditor.cpp:363
msgid "Remove Path"
msgstr "Удалить путь"
#: ../src/LevelEditor.cpp:365 ../src/LevelEditor.cpp:371
#: ../src/LevelEditor.cpp:587 ../src/LevelEditor.cpp:589
msgid "Activated"
msgstr "Активировано"
#: ../src/LevelEditor.cpp:366 ../src/LevelEditor.cpp:612
#: ../src/LevelEditor.cpp:614
msgid "Looping"
msgstr "Повторять"
#: ../src/LevelEditor.cpp:372 ../src/LevelEditor.cpp:3526
msgid "Speed"
msgstr "Скорость"
#: ../src/LevelEditor.cpp:378 ../src/LevelEditor.cpp:668
#, c-format
msgid "State: %s"
msgstr "Состояние: %s"
#: ../src/LevelEditor.cpp:382 ../src/LevelEditor.cpp:3511
msgid "Message"
msgstr "Сообщение"
#: ../src/LevelEditor.cpp:384 ../src/LevelEditor.cpp:1202
#: ../src/LevelEditor.cpp:3825
msgid "Appearance"
msgstr "Внешний вид"
#: ../src/LevelEditor.cpp:389 ../src/LevelEditor.cpp:431
#: ../src/LevelEditor.cpp:715
msgid "Scripting"
msgstr "Скрипты"
#: ../src/LevelEditor.cpp:402 ../src/LevelEditor.cpp:867
#: ../src/LevelEditor.cpp:885
#, c-format
msgid "Background layer: %s"
msgstr "Слой фона: %s"
#: ../src/LevelEditor.cpp:409 ../src/LevelEditor.cpp:866
#: ../src/LevelEditor.cpp:884
msgid "Blocks layer"
msgstr "Сбой блоков"
#: ../src/LevelEditor.cpp:417 ../src/LevelEditor.cpp:867
#: ../src/LevelEditor.cpp:885
#, c-format
msgid "Foreground layer: %s"
msgstr "Передний план: %s"
#: ../src/LevelEditor.cpp:423
msgid "Add new layer"
msgstr "Добавить слой"
#: ../src/LevelEditor.cpp:424
msgid "Delete selected layer"
msgstr "Удалить выбранный слой"
#: ../src/LevelEditor.cpp:425
msgid "Configure selected layer"
msgstr "Настроить выбранный слой"
#: ../src/LevelEditor.cpp:426
msgid "Move selected object to layer"
msgstr "Переместить выбранный объект в слой"
#: ../src/LevelEditor.cpp:430 ../src/OptionsMenu.cpp:55
msgid "Settings"
msgstr "Настройки"
#: ../src/LevelEditor.cpp:463
msgid ""
"NOTE: the layers are sorted by name alphabetically.\n"
"The layer is background layer if its name is < 'f'\n"
"by dictionary order, otherwise it's foreground layer."
msgstr "ЗАМЕТКА: слои отсортированы по названию в алфавитном порядке.\n"
"Слой является фоновым слоем, если его имя < 'f'\n"
"по словарному порядку, в противоположном случае - это слой переднего плана."
#: ../src/LevelEditor.cpp:539
msgid "Notification block"
msgstr "Блок оповещения"
#: ../src/LevelEditor.cpp:545
msgid "Enter message here:"
msgstr "Введите сообщение:"
#: ../src/LevelEditor.cpp:646
msgid "Behavior"
msgstr "Поведение"
#: ../src/LevelEditor.cpp:665
msgid "State"
msgstr "Состояние"
#: ../src/LevelEditor.cpp:673
msgid "Conveyor belt speed"
msgstr "Скорость конвейера"
#: ../src/LevelEditor.cpp:679
msgid "Enter speed here:"
msgstr "Введите скорость:"
#: ../src/LevelEditor.cpp:690
msgid "NOTE: 1 Speed = 0.08 block/s"
msgstr "ПРИМ.: 1 Скорость = 0,08 блоков/с"
#: ../src/LevelEditor.cpp:721
msgid "Id:"
msgstr "Ид:"
#: ../src/LevelEditor.cpp:787
msgid "Level Scripting"
msgstr "Скрипты уровня"
#: ../src/LevelEditor.cpp:892
msgid "Add layer"
msgstr "Добавить слой"
#: ../src/LevelEditor.cpp:898
msgid "Enter the layer name:"
msgstr "Введите название слоя:"
#: ../src/LevelEditor.cpp:943
#, c-format
msgid "Are you sure you want to delete layer '%s'?"
msgstr "Вы уверены, что Вы хотите удалить слой '%s'?"
#: ../src/LevelEditor.cpp:944
msgid "Delete layer"
msgstr "Удалить слой"
#: ../src/LevelEditor.cpp:968
msgid "Layer settings"
msgstr "Настроить слой"
#: ../src/LevelEditor.cpp:974
msgid "Layer name:"
msgstr "Название слоя:"
#: ../src/LevelEditor.cpp:989
msgid "Layer moving speed (1 speed = 0.8 block/s):"
msgstr "Скорость перемещения слоя (1 скорость = 0,08 блоков/с):"
#: ../src/LevelEditor.cpp:1010
msgid "Speed of following camera:"
msgstr "Скорость камеры:"
#: ../src/LevelEditor.cpp:1062
msgid "Move to layer"
msgstr "Переместить в слой"
#: ../src/LevelEditor.cpp:1068
msgid "Enter the layer name (create new layer if necessary):"
msgstr "Введите название слоя (создать новый слой при необходимости):"
#: ../src/LevelEditor.cpp:1132
msgid "Repeat mode"
msgstr "Режим повторения"
#: ../src/LevelEditor.cpp:1156
msgid "Custom scenery:"
msgstr "Фон:"
#: ../src/LevelEditor.cpp:1219
msgid "(Use the default appearance for this block)"
msgstr "(Использовать обычный вид этого блока)"
#: ../src/LevelEditor.cpp:1465 ../src/LevelEditor.cpp:1707
#: ../src/LevelEditor.cpp:1723 ../src/LevelEditor.cpp:1772
#: ../src/LevelEditor.cpp:4400
msgid "Custom scenery block"
msgstr "Блок фона"
#: ../src/LevelEditor.cpp:1673
msgid "Toolbox"
msgstr "Инструменты"
#: ../src/LevelEditor.cpp:1705
#, c-format
msgid "Resize %s"
msgstr "Изменить размер %s"
#: ../src/LevelEditor.cpp:1705
#, c-format
msgid "Move %s"
msgstr "Переместить %s"
#: ../src/LevelEditor.cpp:1713
#, c-format
msgid "Move %d object"
msgid_plural "Move %d objects"
msgstr[0] "Переместить %d объект"
msgstr[1] "Переместить %d объекта"
msgstr[2] "Переместить %d объектов"
msgstr[3] "Переместить %d объектов"
#: ../src/LevelEditor.cpp:1721
#, c-format
msgid "Add %s"
msgstr "Добавить %s"
#: ../src/LevelEditor.cpp:1721
#, c-format
msgid "Remove %s"
msgstr "Удалить %s"
#: ../src/LevelEditor.cpp:1729
#, c-format
msgid "Add %d object"
msgid_plural "Add %d objects"
msgstr[0] "Добавить %d объект"
msgstr[1] "Добавить %d объекта"
msgstr[2] "Добавить %d объектов"
msgstr[3] "Добавить %d объектов"
#: ../src/LevelEditor.cpp:1731
#, c-format
msgid "Remove %d object"
msgid_plural "Remove %d objects"
msgstr[0] "Удалить %d объект"
msgstr[1] "Удалить %d объекта"
msgstr[2] "Удалить %d объектов"
msgstr[3] "Удалить %d объектов"
#: ../src/LevelEditor.cpp:1739
#, c-format
msgid "Add path to %s"
msgstr "Добавить путь к %s"
#: ../src/LevelEditor.cpp:1741
#, c-format
msgid "Remove a path point from %s"
msgstr "Удалить точку пути из %s"
#: ../src/LevelEditor.cpp:1747
#, c-format
msgid "Remove all paths from %s"
msgstr "Удалить все пути из %s"
#: ../src/LevelEditor.cpp:1753
#, c-format
msgid "Add link from %s to %s"
msgstr "Добавить сслыку между %s и %s"
#: ../src/LevelEditor.cpp:1759
#, c-format
msgid "Remove all links from %s"
msgstr "Удалить все ссылки из %s"
#: ../src/LevelEditor.cpp:1766
msgid "Modify the %2 property of %1"
msgstr "Изменить у %1 свойство %2"
#: ../src/LevelEditor.cpp:1818
#, c-format
msgid "Edit the script of %s"
msgstr "Редактировать скрипт %s"
#: ../src/LevelEditor.cpp:1821
msgid "Edit the script of level"
msgstr "Редактировать скрипт уровня"
#: ../src/LevelEditor.cpp:2146 ../src/LevelEditor.cpp:2226
msgid "The level has unsaved changes."
msgstr "В уровне есть несохранённые изменения."
#: ../src/LevelEditor.cpp:2150 ../src/LevelEditor.cpp:2228
msgid "Are you sure you want to quit?"
msgstr "Вы действительно хотите выйти?"
#: ../src/LevelEditor.cpp:2150 ../src/LevelEditor.cpp:2228
msgid "Quit prompt"
msgstr "Подтвердить выход"
#: ../src/LevelEditor.cpp:2797 ../src/LevelEditor.cpp:2799
#, c-format
msgid "Level \"%s\" saved"
msgstr "Уровень \"%s\" сохранён"
#: ../src/LevelEditor.cpp:2797 ../src/LevelEditor.cpp:2799
msgid "Saved"
msgstr "Сохранено"
#: ../src/LevelEditor.cpp:2859 ../src/LevelEditSelect.cpp:208
msgid "Name:"
msgstr "Название:"
#: ../src/LevelEditor.cpp:2866
msgid "Theme:"
msgstr "Тема:"
#: ../src/LevelEditor.cpp:2873
msgid "Examples: %DATA%/themes/classic"
msgstr "Примеры: %DATA%/themes/classic"
#: ../src/LevelEditor.cpp:2875
msgid "or %USER%/themes/Orange"
msgstr "или %USER%/themes/Orange"
#: ../src/LevelEditor.cpp:2878
msgid "Music:"
msgstr "Музыка:"
#: ../src/LevelEditor.cpp:2887
msgid "Target time (s):"
msgstr "Необходимое время (с):"
#: ../src/LevelEditor.cpp:2903
msgid "Target recordings:"
msgstr "Необходимо записей:"
#: ../src/LevelEditor.cpp:2919
msgid "Restart level editor is required"
msgstr "Необходимо перезапустить редактор уровней"
#: ../src/LevelEditor.cpp:3679 ../src/LevelEditor.cpp:3718
#: ../src/LevelEditor.cpp:3739
msgid "Please enter a layer name."
msgstr "Пожалуйста, введите название слоя."
#: ../src/LevelEditor.cpp:3679 ../src/LevelEditor.cpp:3683
#: ../src/LevelEditor.cpp:3718 ../src/LevelEditor.cpp:3722
#: ../src/LevelEditor.cpp:3739 ../src/LevelEditor.cpp:3743
#: ../src/LevelEditSelect.cpp:644 ../src/LevelEditSelect.cpp:683
#: ../src/LevelEditSelect.cpp:688 ../src/LevelEditSelect.cpp:693
#: ../src/LevelEditSelect.cpp:698 ../src/LevelEditSelect.cpp:796
msgid "Error"
msgstr "Ошибка"
#: ../src/LevelEditor.cpp:3683 ../src/LevelEditor.cpp:3722
#, c-format
msgid "The layer '%s' already exists."
msgstr "Слой '%s' уже существует."
#: ../src/LevelEditor.cpp:3743
msgid "Source and destination layers are the same."
msgstr "Исходный и конечный слои совпадают."
#: ../src/LevelEditor.cpp:3760
msgid "Scenery"
msgstr "Фон"
#: ../src/LevelEditor.cpp:4190 ../src/LevelEditor.cpp:4218
#, c-format
msgid "Speed: %d = %0.2f block/s"
msgstr "Скорость: %d = %0.2f блоков/с"
#: ../src/LevelEditor.cpp:4203
msgid "Stop at this point"
msgstr "Остановиться в этой точке"
#: ../src/LevelEditor.cpp:4208
#, c-format
msgid "Pause: %d = %0.3fs"
msgstr "Пауза: %d = %0.3fс"
#: ../src/LevelEditSelect.cpp:41 ../src/TitleMenu.cpp:45
msgid "Map Editor"
msgstr "Редактор карт"
#: ../src/LevelEditSelect.cpp:66
msgid "New Levelpack"
msgstr "Новый сборник уровней"
#: ../src/LevelEditSelect.cpp:71
msgid "Pack Properties"
msgstr "Свойства сборника"
#: ../src/LevelEditSelect.cpp:76
msgid "Remove Pack"
msgstr "Удалить сборник"
#: ../src/LevelEditSelect.cpp:81
msgid "Move Map"
msgstr "Переместить карту"
#: ../src/LevelEditSelect.cpp:89
msgid "Remove Map"
msgstr "Удалить карту"
#: ../src/LevelEditSelect.cpp:94
msgid "Edit Map"
msgstr "Редактировать карту"
#: ../src/LevelEditSelect.cpp:205
msgid "Properties"
msgstr "Свойства"
#: ../src/LevelEditSelect.cpp:217
msgid "Description:"
msgstr "Описание:"
#: ../src/LevelEditSelect.cpp:226
msgid "Congratulation text:"
msgstr "Текст поздравления:"
#: ../src/LevelEditSelect.cpp:235
msgid "Music list:"
msgstr "Список музыки:"
#: ../src/LevelEditSelect.cpp:265 ../src/LevelEditSelect.cpp:485
msgid "Add level"
msgstr "Добавить уровень"
#: ../src/LevelEditSelect.cpp:268
msgid "File name:"
msgstr "Название файла:"
#: ../src/LevelEditSelect.cpp:293
msgid "Move level"
msgstr "Переместить уровень"
#: ../src/LevelEditSelect.cpp:296
msgid "Level: "
msgstr "Уровень: "
#: ../src/LevelEditSelect.cpp:310
msgid "Before"
msgstr "До"
#: ../src/LevelEditSelect.cpp:311
msgid "After"
msgstr "После"
#: ../src/LevelEditSelect.cpp:368 ../src/LevelPlaySelect.cpp:124
msgid "Individual levels which are not contained in any level packs"
msgstr "Отдельные уровни, которые не содержатся ни в одном сборнике уровней"
#: ../src/LevelEditSelect.cpp:577
#, c-format
msgid "Are you sure remove the level pack '%s'?"
msgstr "Вы уверены, что хотите удалить сборник уровней '%s'?"
#: ../src/LevelEditSelect.cpp:577 ../src/LevelEditSelect.cpp:607
msgid "Remove prompt"
msgstr "Подтвердить удаление"
#: ../src/LevelEditSelect.cpp:607
#, c-format
msgid "Are you sure remove the map '%s'?"
msgstr "Вы уверены, что хотите удалить карту '%s'?"
#: ../src/LevelEditSelect.cpp:644
msgid "Levelpack name cannot be empty."
msgstr "Название сборника уровней не может быть пустым."
#: ../src/LevelEditSelect.cpp:683
#, c-format
msgid "The levelpack directory '%s' already exists!"
msgstr "Папка сборника уровней '%s' уже существует!"
#: ../src/LevelEditSelect.cpp:688
#, c-format
msgid "Unable to create levelpack directory '%s'!"
msgstr "Не удалось создать папку сборника уровней '%s'!"
#: ../src/LevelEditSelect.cpp:693
#, c-format
msgid "The levelpack file '%s' already exists!"
msgstr "Файл сборника уровней '%s' уже существует!"
#: ../src/LevelEditSelect.cpp:698
#, c-format
msgid "Unable to create levelpack file '%s'!"
msgstr "Не удалось создать файл сборника уровней '%s'!"
#: ../src/LevelEditSelect.cpp:758
msgid "No file name given for the new level."
msgstr "Новому уровню не было присвоено имя файла."
#: ../src/LevelEditSelect.cpp:758
msgid "Missing file name"
msgstr "Отсутствует имя файла"
#: ../src/LevelEditSelect.cpp:796
#, c-format
msgid "The file %s already exists."
msgstr "Файл %s уже существует."
#: ../src/LevelEditSelect.cpp:849
msgid "The entered level number isn't valid!"
msgstr "Введённый номер уровня некорректен!"
#: ../src/LevelEditSelect.cpp:849
msgid "Illegal number"
msgstr "Некорректное число"
#: ../src/LevelInfoRender.cpp:19
msgid "Choose a level"
msgstr "Выберите уровень"
#: ../src/LevelInfoRender.cpp:20
msgid "Time:"
msgstr "Время:"
#: ../src/LevelInfoRender.cpp:21 ../src/StatisticsScreen.cpp:259
msgid "Recordings:"
msgstr "Записей:"
#: ../src/LevelPackManager.cpp:124
msgid "Custom Levels"
msgstr "Другие уровни"
#: ../src/LevelPlaySelect.cpp:41
msgid "Select Level"
msgstr "Выберите уровень"
#: ../src/OptionsMenu.cpp:66
msgid "Clear Progress"
msgstr "Очистить прогресс"
#: ../src/OptionsMenu.cpp:109
msgid "General"
msgstr "Общие"
#: ../src/OptionsMenu.cpp:110
msgid "Controls"
msgstr "Управление"
#: ../src/OptionsMenu.cpp:121
msgid "Music"
msgstr "Музыка"
#: ../src/OptionsMenu.cpp:129
msgid "Sound"
msgstr "Звуки"
#: ../src/OptionsMenu.cpp:137
msgid "Resolution"
msgstr "Разрешение"
#: ../src/OptionsMenu.cpp:177
msgid "Language"
msgstr "Язык"
#: ../src/OptionsMenu.cpp:185
msgid "Auto-Detect"
msgstr "Автоопределение"
#: ../src/OptionsMenu.cpp:209
msgid "Theme"
msgstr "Тема"
#: ../src/OptionsMenu.cpp:247
msgid "Internet proxy"
msgstr "Интернет-прокси"
#: ../src/OptionsMenu.cpp:256
msgid "Fullscreen"
msgstr "На весь экран"
#: ../src/OptionsMenu.cpp:261
msgid "Quick record"
msgstr "Быстрая запись"
#: ../src/OptionsMenu.cpp:266
msgid "Internet"
msgstr "Интернет"
#: ../src/OptionsMenu.cpp:271
msgid "Fade transition"
msgstr "Плавный переход"
#: ../src/OptionsMenu.cpp:294
msgid "Save Changes"
msgstr "Сохранить изменения"
#: ../src/OptionsMenu.cpp:513
msgid "Do you really want to reset level progress?"
msgstr "Вы действительно хотите очистить прогресс уровня?"
#: ../src/OptionsMenu.cpp:513
msgid "Warning"
msgstr "Внимание"
#: ../src/StatisticsManager.cpp:386
msgid "New achievement:"
msgstr "Новое достижение:"
#: ../src/StatisticsManager.cpp:394
#, c-format
msgid "Achieved on %s"
msgstr "Достижение %s"
#: ../src/StatisticsManager.cpp:400
msgid "Unknown achievement"
msgstr "Неизвестное достижение"
#: ../src/StatisticsManager.cpp:406
#, c-format
msgid "Achieved %1.0f%%"
msgstr "Достижение %1.0f%%"
#: ../src/StatisticsManager.cpp:410
msgid "Not achieved"
msgstr "Не достигнуто"
#: ../src/StatisticsScreen.cpp:57 ../src/TitleMenu.cpp:55
msgid "Achievements and Statistics"
msgstr "Достижения и статистика"
#: ../src/StatisticsScreen.cpp:166
msgid "Achievements"
msgstr "Достижения"
#: ../src/StatisticsScreen.cpp:167
msgid "Statistics"
msgstr "Статистика"
#: ../src/StatisticsScreen.cpp:234
msgid "Total"
msgstr "Всего"
#: ../src/StatisticsScreen.cpp:246
msgid "Traveling distance (m)"
msgstr "Пройденное расстояние (м)"
#: ../src/StatisticsScreen.cpp:247
msgid "Jump times"
msgstr "Прыжков"
#: ../src/StatisticsScreen.cpp:248
msgid "Die times"
msgstr "Смертей"
#: ../src/StatisticsScreen.cpp:249
msgid "Squashed times"
msgstr "Раздавлен"
#: ../src/StatisticsScreen.cpp:260
msgid "Switch pulled times:"
msgstr "Включено выключателей:"
#: ../src/StatisticsScreen.cpp:261
msgid "Swap times:"
msgstr "Поменялся:"
#: ../src/StatisticsScreen.cpp:262
msgid "Save times:"
msgstr "Сохранений:"
#: ../src/StatisticsScreen.cpp:263
msgid "Load times:"
msgstr "Загрузок:"
#: ../src/StatisticsScreen.cpp:268
msgid "Completed levels:"
msgstr "Завершённых уровней:"
#: ../src/StatisticsScreen.cpp:306
msgid "In-game time:"
msgstr "Время в игре:"
#: ../src/StatisticsScreen.cpp:308
msgid "Level editing time:"
msgstr "Время в редакторе:"
#: ../src/StatisticsScreen.cpp:310
msgid "Created levels:"
msgstr "Создано уровней:"
#: ../src/TitleMenu.cpp:44
msgid "Options"
msgstr "Настройки"
#: ../src/TitleMenu.cpp:47
msgid "Quit"
msgstr "Выход"
#: ../src/TitleMenu.cpp:131
msgid "Enable internet in order to install addons."
msgstr "Чтобы устанавливать дополнения, разрешите подключение к Интернету."
#: ../src/TitleMenu.cpp:131
msgid "Internet disabled"
msgstr "Подключение к Интернету запрещено"
msgctxt "keys"
msgid "Return"
msgstr "Return"
msgctxt "keys"
msgid "Escape"
msgstr "Escape"
msgctxt "keys"
msgid "Backspace"
msgstr "Backspace"
msgctxt "keys"
msgid "Tab"
msgstr "Tab"
msgctxt "keys"
msgid "Space"
msgstr "Пробел"
msgctxt "keys"
msgid "CapsLock"
msgstr "CapsLock"
msgctxt "keys"
msgid "PrintScreen"
msgstr "PrintScreen"
msgctxt "keys"
msgid "ScrollLock"
msgstr "ScrollLock"
msgctxt "keys"
msgid "Pause"
msgstr "Pause"
msgctxt "keys"
msgid "Insert"
msgstr "Insert"
msgctxt "keys"
msgid "Home"
msgstr "Home"
msgctxt "keys"
msgid "PageUp"
msgstr "PageUp"
msgctxt "keys"
msgid "Delete"
msgstr "Delete"
msgctxt "keys"
msgid "End"
msgstr "End"
msgctxt "keys"
msgid "PageDown"
msgstr "PageDown"
msgctxt "keys"
msgid "Right"
msgstr "Вправо"
msgctxt "keys"
msgid "Left"
msgstr "Влево"
msgctxt "keys"
msgid "Down"
msgstr "Вниз"
msgctxt "keys"
msgid "Up"
msgstr "Вверх"
msgctxt "keys"
msgid "Numlock"
msgstr "Numlock"
msgctxt "keys"
msgid "SysReq"
msgstr "SysReq"
msgctxt "keys"
msgid "Left Ctrl"
msgstr "Левый Ctrl"
msgctxt "keys"
msgid "Left Shift"
msgstr "Левый Shift"
msgctxt "keys"
msgid "Left Alt"
msgstr "Левый Alt"
msgctxt "keys"
msgid "Left GUI"
msgstr "Левый GUI"
msgctxt "keys"
msgid "Right Ctrl"
msgstr "Правый Ctrl"
msgctxt "keys"
msgid "Right Shift"
msgstr "Правый Shift"
msgctxt "keys"
msgid "Right Alt"
msgstr "Правый Alt"
msgctxt "keys"
msgid "Right GUI"
msgstr "Правый GUI"
diff --git a/data/locale/uk.po b/data/locale/uk.po
index 3b17f28..a573de6 100644
--- a/data/locale/uk.po
+++ b/data/locale/uk.po
@@ -1,2110 +1,2111 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the meandmyshadow package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: meandmyshadow 0.5svn\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-09-09 16:18+0800\n"
-"PO-Revision-Date: 2018-09-13 12:13+0000\n"
-"Last-Translator: Jz Pan <acme_pjz@hotmail.com>\n"
-"Language-Team: Ukrainian <https://hosted.weblate.org/projects/me-and-my-shadow/"
-"translations/uk/>\n"
+"PO-Revision-Date: 2018-10-24 16:42+0000\n"
+"Last-Translator: Olexandr Nesterenko <olexn@ukr.net>\n"
+"Language-Team: Ukrainian <https://hosted.weblate.org/projects/"
+"me-and-my-shadow/translations/uk/>\n"
"Language: uk\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
-"n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || "
-"(n%100>=11 && n%100<=14)? 2 : 3);\n"
-"X-Generator: Weblate 3.2-dev\n"
+"Plural-Forms: nplurals=4; plural=(n % 1 == 0 && n % 10 == 1 && n % 100 != 11 "
+"? 0 : n % 1 == 0 && n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > "
+"14) ? 1 : n % 1 == 0 && (n % 10 ==0 || (n % 10 >=5 && n % 10 <=9) || (n % "
+"100 >=11 && n % 100 <=14 )) ? 2: 3);\n"
+"X-Generator: Weblate 3.3-dev\n"
#: ../src/AchievementList.cpp:43
msgid "Newbie"
msgstr "Початківець"
#: ../src/AchievementList.cpp:43
msgid "Complete a level."
msgstr "Завершити один рівень."
#: ../src/AchievementList.cpp:44
msgid "Experienced player"
msgstr "Досвідчений гравець"
#: ../src/AchievementList.cpp:44
msgid "Complete 50 levels."
msgstr "Завершити 50 рівнів."
#: ../src/AchievementList.cpp:45
msgid "Good job!"
msgstr "Молодець!"
#: ../src/AchievementList.cpp:45
msgid "Receive a gold medal."
msgstr "Отримати золоту медаль."
#: ../src/AchievementList.cpp:46
msgid "Expert"
msgstr "Експерт"
#: ../src/AchievementList.cpp:46
msgid "Earn 50 gold medal."
msgstr "Отримати 50 золотих медалей."
#: ../src/AchievementList.cpp:48
msgid "Graduate"
msgstr "Випускник"
#: ../src/AchievementList.cpp:48
msgid "Complete the tutorial level pack."
msgstr "Завершити збірник навчальних рівнів."
#: ../src/AchievementList.cpp:49
msgid "Outstanding graduate"
msgstr "Відмінник"
#: ../src/AchievementList.cpp:49
msgid "Complete the tutorial level pack with gold for all levels."
msgstr "Завершити усі навчальні рівні із золотими медалями."
#: ../src/AchievementList.cpp:51
msgid "Hooked"
msgstr "Засів надовго"
#: ../src/AchievementList.cpp:51
msgid "Play Me and My Shadow for more than 2 hours."
msgstr "Грати у Me and My Shadow більше двох годин."
#: ../src/AchievementList.cpp:52
msgid "Loyal fan of Me and My Shadow"
msgstr "Вірний фанат"
#: ../src/AchievementList.cpp:52
msgid "Play Me and My Shadow for more than 24 hours."
msgstr "Грати у Me and My Shadow довше 24 годин."
#: ../src/AchievementList.cpp:54
msgid "Constructor"
msgstr "Конструктор"
#: ../src/AchievementList.cpp:54
msgid "Use the level editor for more than 2 hours."
msgstr "Використовувати редактор рівнів більше двох годин."
#: ../src/AchievementList.cpp:55
msgid "The creator"
msgstr "Творець"
#: ../src/AchievementList.cpp:55
msgid "Use the level editor for more than 24 hours."
msgstr "Використовувати редактор рівнів більше 24 годин."
#: ../src/AchievementList.cpp:57
msgid "Look, cute level!"
msgstr "Ой, яке ж воно файне!"
#: ../src/AchievementList.cpp:57
msgid "Create a level for the first time."
msgstr "Створити свій перший рівень."
#: ../src/AchievementList.cpp:58
msgid "The level museum"
msgstr "Музей рівнебудівництва"
#: ../src/AchievementList.cpp:58
msgid "Create 50 levels."
msgstr "Створити 50 рівнів."
#: ../src/AchievementList.cpp:60
msgid "Hello, World!"
msgstr "Hello, World!"
#: ../src/AchievementList.cpp:60
msgid "Write a script for the first time."
msgstr "Написати свій перший скрипт."
#: ../src/AchievementList.cpp:62
msgid "Frog"
msgstr "Коник-стрибунець"
#: ../src/AchievementList.cpp:62
msgid "Jump 1000 times."
msgstr "Стрибнути 1000 разів."
#: ../src/AchievementList.cpp:64
msgid "Wanderer"
msgstr "Пішохід"
#: ../src/AchievementList.cpp:64
msgid "Travel 100 meters."
msgstr "Пройти 100 метрів."
#: ../src/AchievementList.cpp:65
msgid "Runner"
msgstr "Мандрівник"
#: ../src/AchievementList.cpp:65
msgid "Travel 1 kilometer."
msgstr "Пройти 1 кілометр."
#: ../src/AchievementList.cpp:66
msgid "Long distance runner"
msgstr "Дальнобійник"
#: ../src/AchievementList.cpp:66
msgid "Travel 10 kilometers."
msgstr "Пройти 10 кілометрів."
#: ../src/AchievementList.cpp:67
msgid "Marathon runner"
msgstr "Марафонець"
#: ../src/AchievementList.cpp:67
msgid "Travel 42,195 meters."
msgstr "Пройти 42195 метрів."
#: ../src/AchievementList.cpp:69
msgid "Be careful!"
msgstr "Обережно!"
#: ../src/AchievementList.cpp:69
msgid "Die for the first time."
msgstr "Вмерти вперше."
#: ../src/AchievementList.cpp:70
msgid "It doesn't matter..."
msgstr "Я вже звикаю..."
#: ../src/AchievementList.cpp:70
msgid "Die 50 times."
msgstr "Померти 50 разів."
#: ../src/AchievementList.cpp:71
msgid "Expert of trial and error"
msgstr "Досвідчений мрець"
#: ../src/AchievementList.cpp:71
msgid "Die 1000 times."
msgstr "Померти 1000 разів."
#: ../src/AchievementList.cpp:73
msgid "Keep an eye for moving blocks!"
msgstr "Стережись рухомих блоків!"
#: ../src/AchievementList.cpp:73
msgid "Get squashed for the first time."
msgstr "Бути розчавленим уперше."
#: ../src/AchievementList.cpp:74
msgid "Potato masher"
msgstr "Коцюрба"
#: ../src/AchievementList.cpp:74
msgid "Get squashed 50 times."
msgstr "Бути розчавленим 50 разів."
#: ../src/AchievementList.cpp:76
msgid "Double kill"
msgstr "За двома зайцями"
#: ../src/AchievementList.cpp:76
msgid "Get both the player and the shadow dead."
msgstr "І гравець і тінь померли."
#: ../src/AchievementList.cpp:78
msgid "Bad luck"
msgstr "От, халепа"
#: ../src/AchievementList.cpp:78
msgid "Die 5 times in under 5 seconds."
msgstr "Померти 5 разів менш ніж за 5 секунд."
#: ../src/AchievementList.cpp:79
msgid "This level is too dangerous"
msgstr "Хай йому грець"
#: ../src/AchievementList.cpp:79
msgid "Die 10 times in under 5 seconds."
msgstr "Померти 10 разів менш ніж за 5 секунд."
#: ../src/AchievementList.cpp:81
msgid "You forgot your friend"
msgstr "Друзів не лишають у біді"
#: ../src/AchievementList.cpp:81
msgid "Finish the level with the player or the shadow dead."
msgstr "Завершити рівень при тому, що або гравець, або тінь померли."
#: ../src/AchievementList.cpp:82
msgid "Just in time"
msgstr "У самісіньке яблучко"
#: ../src/AchievementList.cpp:82
msgid "Reach the exit with the player and the shadow simultaneously."
msgstr "Одночасно досягнути виходу і гравцем, і тінню."
#: ../src/AchievementList.cpp:84
msgid "Recorder"
msgstr "Рекордсмен"
#: ../src/AchievementList.cpp:84
msgid "Record 100 times."
msgstr "Поставити рекорд 100 разів."
#: ../src/AchievementList.cpp:85
msgid "Shadowmaster"
msgstr "Чемпіон"
#: ../src/AchievementList.cpp:85
msgid "Record 1000 times."
msgstr "Поставити рекорд 1000 разів."
#: ../src/AchievementList.cpp:87
msgid "Switch puller"
msgstr "Вмикач вимикачів"
#: ../src/AchievementList.cpp:87
msgid "Pull the switch 100 times."
msgstr "Увімкнути вимикач 100 разів."
#: ../src/AchievementList.cpp:88
msgid "The switch is broken!"
msgstr "Псувач вимикачів"
#: ../src/AchievementList.cpp:88
msgid "Pull the switch 1000 times."
msgstr "Увімкнути вимикач 1000 разів."
#: ../src/AchievementList.cpp:90
msgid "Swapper"
msgstr "Ся міняю"
#: ../src/AchievementList.cpp:90
msgid "Swap 100 times."
msgstr "Помінятися 100 разів."
#: ../src/AchievementList.cpp:91
msgid "Player to shadow to player to shadow..."
msgstr "Туди-сюдитель"
#: ../src/AchievementList.cpp:91
msgid "Swap 1000 times."
msgstr "Помінятися 1000 разів."
#: ../src/AchievementList.cpp:93
msgid "Play it save"
msgstr "Добре зберігся"
#: ../src/AchievementList.cpp:93
msgid "Save 1000 times."
msgstr "Зберегти гру 1000 разів."
#: ../src/AchievementList.cpp:94
msgid "This game is too hard"
msgstr "Ох і важко ж"
#: ../src/AchievementList.cpp:94
msgid "Load the game 1000 times."
msgstr "Завантажити гру 1000 разів."
#: ../src/AchievementList.cpp:96
msgid "No, thanks"
msgstr "Ні, дякую"
#: ../src/AchievementList.cpp:96
msgid "Complete a level with checkpoint, but without saving."
msgstr "Пройти рівень із чекпоінтом, але без збережень гри."
#: ../src/AchievementList.cpp:98
msgid "Panic save"
msgstr "Самозбережник"
#: ../src/AchievementList.cpp:98
msgid "Save twice in 1 second."
msgstr "Зберегти гру двічі за секунду."
#: ../src/AchievementList.cpp:99
msgid "Panic load"
msgstr "Панічне завантаження"
#: ../src/AchievementList.cpp:99
msgid "Load twice in 1 second."
msgstr "Завантажити гру двічі за секунду."
#: ../src/AchievementList.cpp:101
msgid "Bad saving position"
msgstr "Кепське місце для збереження"
#: ../src/AchievementList.cpp:101
msgid "Load the game and die within 1 second."
msgstr "Завантажити гру й померти протягом одної секунди."
#: ../src/AchievementList.cpp:102
msgid "This level is too hard"
msgstr "Заважкий рівень"
#: ../src/AchievementList.cpp:102
msgid "Load the same save and die 100 times."
msgstr "Завантажити гру і померти 100 разів."
#: ../src/AchievementList.cpp:104
msgid "Quick swap"
msgstr "Швидка зміна"
#: ../src/AchievementList.cpp:104
msgid "Swap twice in under a second."
msgstr "Помінятися двічі на секунду."
#: ../src/AchievementList.cpp:107
msgid "Horizontal confusion"
msgstr "Забув де право, а де ліво"
#: ../src/AchievementList.cpp:107
msgid "Press left and right simultaneously."
msgstr "Натиснути ліворуч і праворуч одночасно."
#: ../src/AchievementList.cpp:109
msgid "Cheater"
msgstr "Нечемний"
#: ../src/AchievementList.cpp:109
msgid "Cheat in game."
msgstr "Змухлювати у грі."
#: ../src/AchievementList.cpp:111
msgid "Programmer"
msgstr "Тижпрограміст"
#: ../src/AchievementList.cpp:111
msgid "Play the development version of Me and My Shadow."
msgstr "Грати у версію у розробці."
#: ../src/Addons.cpp:44 ../src/LevelPackManager.cpp:108
msgid "Levels"
msgstr "Рівні"
#: ../src/Addons.cpp:44
msgid "Single level which usually contain demanding puzzles"
msgstr "Один рівень, що зазвичай містить головоломки"
#: ../src/Addons.cpp:45
msgid "Levelpacks"
msgstr "Збірка рівнів"
#: ../src/Addons.cpp:45
msgid "Collection of levels with the same author or style"
msgstr "Колекція рівнів від одного автора чи із подібним стилем"
#: ../src/Addons.cpp:46
msgid "Themes"
msgstr "Оформлення"
#: ../src/Addons.cpp:46
msgid "Give every block and background a new look and feel"
msgstr "Надати кожному блоку й тлу новий вигляд і відчуття"
#: ../src/Addons.cpp:55 ../src/TitleMenu.cpp:46
msgid "Addons"
msgstr "Застосунки"
#: ../src/Addons.cpp:87
msgid "Unable to initialize addon menu:"
msgstr "Не вдалося ініціалізувати меню застосунків:"
#: ../src/Addons.cpp:95 ../src/Addons.cpp:158 ../src/Addons.cpp:662
#: ../src/Addons.cpp:690 ../src/CreditsMenu.cpp:89 ../src/LevelSelect.cpp:168
#: ../src/StatisticsScreen.cpp:159
msgid "Back"
msgstr "Повернутися"
#: ../src/Addons.cpp:169
msgid "ERROR: unable to download addons file!"
msgstr "ПОМИЛКА: неможливо зкачати файл застосунку!"
# TRANSLATORS: addon_list is the name of a file and should not be translated.
#: ../src/Addons.cpp:182
msgid "ERROR: unable to load addon_list file!"
msgstr "ПОМИЛКА: неможливо завантажити файл addon_list!"
#: ../src/Addons.cpp:193
msgid "ERROR: Invalid file format of addons file!"
msgstr "ПОМИЛКА: Невірний формат файлу застосунку!"
#: ../src/Addons.cpp:205
msgid "ERROR: Addon list version is unsupported!"
msgstr "ПОМИЛКА: Не підтримується версія списку застосунків!"
# TRANSLATORS: installed_addons is the name of a file and should not be
# translated.
#: ../src/Addons.cpp:226
msgid "ERROR: Unable to create the installed_addons file."
msgstr "ПОМИЛКА: Неможливо створити файл installed_addons."
#: ../src/Addons.cpp:238
msgid "ERROR: Invalid file format of the installed_addons!"
msgstr "ПОМИЛКА: Невірний формат файлу installed_addons!"
# TRANSLATORS: indicates the author of an addon.
#: ../src/Addons.cpp:389 ../src/Addons.cpp:621
#, c-format
msgid "by %s"
msgstr "від %s"
#: ../src/Addons.cpp:397
msgid "Installed"
msgstr "Установлено"
#: ../src/Addons.cpp:402
msgid "Updatable"
msgstr "Оновити"
#: ../src/Addons.cpp:412
msgid "Not installed"
msgstr "Не встановлено"
#: ../src/Addons.cpp:625
#, c-format
msgid "Version: %d\n"
msgstr "Версія: %d\n"
#: ../src/Addons.cpp:627
#, c-format
msgid "Installed version: %d\n"
msgstr "Установлена версія: %d\n"
#: ../src/Addons.cpp:630
#, c-format
msgid "License: %s\n"
msgstr "Ліцензія: %s\n"
#: ../src/Addons.cpp:633
#, c-format
msgid "Website: %s\n"
msgstr "Інтернет-сторінка: %s\n"
#: ../src/Addons.cpp:637
msgid "(No descriptions provided)"
msgstr "(Немає опису)"
#: ../src/Addons.cpp:657 ../src/Addons.cpp:684
msgid "Remove"
msgstr "Видалити"
#: ../src/Addons.cpp:673
msgid "Update"
msgstr "Оновити"
#: ../src/Addons.cpp:679
msgid "Install"
msgstr "Установити"
#: ../src/Addons.cpp:774
#, c-format
msgid "This addon can't be removed because it's needed by %s."
msgstr "Цей застосунок неможливо видалити, оскільки його потребує %s."
#: ../src/Addons.cpp:774 ../src/Addons.cpp:1051
msgid "Dependency"
msgstr "Залежить"
#: ../src/Addons.cpp:803
#, c-format
msgid "WARNING: File '%s' appears to have been removed already."
msgstr "УВАГА: Схоже, що файл '%s' вже був видалений."
#: ../src/Addons.cpp:803 ../src/Addons.cpp:810 ../src/Addons.cpp:818
#: ../src/Addons.cpp:825 ../src/Addons.cpp:834 ../src/Addons.cpp:840
#: ../src/Addons.cpp:859 ../src/Addons.cpp:866 ../src/Addons.cpp:893
#: ../src/Addons.cpp:900 ../src/Addons.cpp:907 ../src/Addons.cpp:918
#: ../src/Addons.cpp:947 ../src/Addons.cpp:952 ../src/Addons.cpp:962
#: ../src/Addons.cpp:968 ../src/Addons.cpp:981 ../src/Addons.cpp:986
#: ../src/Addons.cpp:1008 ../src/Addons.cpp:1014 ../src/Addons.cpp:1044
msgid "Addon error"
msgstr "Помилка застосунку"
#: ../src/Addons.cpp:810
#, c-format
msgid "ERROR: Unable to remove file '%s'!"
msgstr "ПОМИЛКА: Неможливо видалити файл '%s'!"
#: ../src/Addons.cpp:818
#, c-format
msgid "WARNING: Directory '%s' appears to have been removed already."
msgstr "УВАГА: Схоже, що директорія '%s' вже була видалена."
#: ../src/Addons.cpp:825
#, c-format
msgid "ERROR: Unable to remove directory '%s'!"
msgstr "ПОМИЛКА: Неможливо видалити директорію '%s'!"
#: ../src/Addons.cpp:834
#, c-format
msgid "WARNING: Level '%s' appears to have been removed already."
msgstr "УВАГА: Схоже, що рівень '%s' вже був видалений."
#: ../src/Addons.cpp:840
#, c-format
msgid "ERROR: Unable to remove level '%s'!"
msgstr "ПОМИЛКА: Неможливо видалити рівень '%s'!"
#: ../src/Addons.cpp:859
#, c-format
msgid "WARNING: Levelpack directory '%s' appears to have been removed already."
msgstr "УВАГА: Схоже, що директорія збірки рівнів '%s' вже була видалена."
#: ../src/Addons.cpp:866
#, c-format
msgid "ERROR: Unable to remove levelpack directory '%s'!"
msgstr "ПОМИЛКА: Неможливо створити директорію збірки рівнів '%s'!"
#: ../src/Addons.cpp:893
#, c-format
msgid "ERROR: Unable to download addon file %s."
msgstr "ПОМИЛКА: Неможливо зкачати файл застосунку %s."
#: ../src/Addons.cpp:900
#, c-format
msgid "ERROR: Unable to extract addon file %s."
msgstr "ПОМИЛКА: Неможливо розпакувати файл застосунку %s."
#: ../src/Addons.cpp:907
msgid "ERROR: Addon is missing metadata!"
msgstr "ПОМИЛКА: Застосунок не має метаданих!"
#: ../src/Addons.cpp:918
msgid "ERROR: Invalid file format for metadata file!"
msgstr "ПОМИЛКА: Невірний формат файлу метаданих!"
#: ../src/Addons.cpp:947
#, c-format
msgid "WARNING: File '%s' already exists, addon may be broken or not working!"
msgstr "УВАГА: Файл '%s' вже існує, застосунок може бути пошкодженим або непрацездатним!"
#: ../src/Addons.cpp:952
#, c-format
msgid ""
"WARNING: Unable to copy file '%s' to '%s', addon may be broken or not "
"working!"
msgstr "УВАГА: Неможливо скопіювати файл '%s' до '%s', застосунок може бути пошкодженим або непрацездатним!"
#: ../src/Addons.cpp:962
#, c-format
msgid ""
"WARNING: Destination directory '%s' already exists, addon may be broken or "
"not working!"
msgstr "УВАГА: Цільова директорія '%s' вже існує, застосунок може бути пошкодженим або непрацездатним!"
#: ../src/Addons.cpp:968 ../src/Addons.cpp:1014
#, c-format
msgid ""
"WARNING: Unable to move directory '%s' to '%s', addon may be broken or not "
"working!"
msgstr "УВАГА: Неможливо перемістити директорію '%s' до '%s', застосунок може бути пошкодженим або непрацездатним!"
#: ../src/Addons.cpp:981
#, c-format
msgid "WARNING: Level '%s' already exists, addon may be broken or not working!"
msgstr "УВАГА: Рівень '%s' вже існує, застосунок може бути пошкодженим або непрацездатним!"
#: ../src/Addons.cpp:986
#, c-format
msgid ""
"WARNING: Unable to copy level '%s' to '%s', addon may be broken or not "
"working!"
msgstr "УВАГА: Неможливо скопіювати рівень '%s' до '%s', застосунок може бути пошкодженим або непрацездатним!"
#: ../src/Addons.cpp:1008
#, c-format
msgid ""
"WARNING: Levelpack directory '%s' already exists, addon may be broken or not "
"working!"
msgstr "УВАГА: Директорія зі збіркою рівнів '%s' вже існує, застосунок може бути пошкодженим або непрацездатним!"
#: ../src/Addons.cpp:1044
#, c-format
msgid "ERROR: Addon requires another addon (%s) which can't be found!"
msgstr "ПОМИЛКА: Застосунок потребує інший застосунок (%s), який не вдалося знайти!"
#: ../src/Addons.cpp:1051
#, c-format
msgid "The addon %s is needed and will be installed now."
msgstr "Потрібний застосунок %s буде зараз установлено."
#: ../src/Block.cpp:822 ../src/LevelEditor.cpp:265
msgid "On"
msgstr "Увімк."
#: ../src/Block.cpp:823 ../src/LevelEditor.cpp:266
msgid "Off"
msgstr "Вимк."
#: ../src/CommandManager.cpp:41
#, c-format
msgid "Undo %s"
msgstr "Відмінити %s"
#: ../src/CommandManager.cpp:43
msgid "Can't undo"
msgstr "Неможливо відмінити"
#: ../src/CommandManager.cpp:49
#, c-format
msgid "Redo %s"
msgstr "Повернути %s"
#: ../src/CommandManager.cpp:51
msgid "Can't redo"
msgstr "Неможливо повернути"
# TRANSLATORS: Context: Undo/Redo ...
#: ../src/Commands.cpp:190
msgid "Resize level"
msgstr "Змінити розмір рівня"
# TRANSLATORS: Context: Undo/Redo ...
#: ../src/Commands.cpp:807
msgid "Modify level property"
msgstr "Змінити властивості рівня"
# TRANSLATORS: Context: Undo/Redo ...
#: ../src/Commands.cpp:919
#, c-format
msgid "Add scenery layer %s"
msgstr "Додати фоновий шар %s"
# TRANSLATORS: Context: Undo/Redo ...
#: ../src/Commands.cpp:921
#, c-format
msgid "Delete scenery layer %s"
msgstr "Видалити фоновий шар %s"
# TRANSLATORS: Context: Undo/Redo ...
#: ../src/Commands.cpp:951
#, c-format
msgid "Modify the property of scenery layer %s"
msgstr "Змінити властивості фонового шару %s"
# TRANSLATORS: Context: Undo/Redo ...
#: ../src/Commands.cpp:1040
#, c-format
msgid "Move %d object from layer %s to layer %s"
msgid_plural "Move %d objects from layer %s to layer %s"
msgstr[0] "Перемістити %d об'єкт із шару %s у шар %s"
msgstr[1] "Перемістити %d об'єкти із шару %s у шар %s"
msgstr[2] "Перемістити %d об'єктів із шару %s у шар %s"
msgstr[3] "Перемістити %d об'єктів із шару %s у шар %s"
#: ../src/CreditsMenu.cpp:35 ../src/TitleMenu.cpp:53
msgid "Credits"
-msgstr "Титри"
+msgstr "Подяки"
# TRANSLATORS: Font used in GUI:
# - Use "knewave" for languages using Latin and Latin-derived alphabets
# - "DroidSansFallback" can be used for non-Latin writing systems
#: ../src/Functions.cpp:569 ../src/Functions.cpp:570 ../src/Functions.cpp:571
#: ../src/Functions.cpp:588
msgid "knewave"
msgstr "DejaVuSansCondensed-Oblique"
# TRANSLATORS: Font used for normal text:
# - Use "Blokletters-Viltstift" for languages using Latin and Latin-derived
# alphabets
# - "DroidSansFallback" can be used for non-Latin writing systems
#: ../src/Functions.cpp:575
msgid "Blokletters-Viltstift"
msgstr "DejaVuSansCondensed-Oblique"
#: ../src/Functions.cpp:674
msgid "Loading..."
msgstr "Завантаження..."
#: ../src/Functions.cpp:1243 ../src/Functions.cpp:1270
#: ../src/LevelEditor.cpp:559 ../src/LevelEditor.cpp:693
#: ../src/LevelEditor.cpp:758 ../src/LevelEditor.cpp:821
#: ../src/LevelEditor.cpp:908 ../src/LevelEditor.cpp:1033
#: ../src/LevelEditor.cpp:1083 ../src/LevelEditor.cpp:1180
#: ../src/LevelEditor.cpp:1244 ../src/LevelEditor.cpp:2923
#: ../src/LevelEditSelect.cpp:244 ../src/LevelEditSelect.cpp:277
#: ../src/LevelEditSelect.cpp:317
msgid "OK"
msgstr "Так"
#: ../src/Functions.cpp:1244 ../src/Functions.cpp:1256
#: ../src/Functions.cpp:1266 ../src/LevelEditor.cpp:565
#: ../src/LevelEditor.cpp:699 ../src/LevelEditor.cpp:764
#: ../src/LevelEditor.cpp:827 ../src/LevelEditor.cpp:914
#: ../src/LevelEditor.cpp:1039 ../src/LevelEditor.cpp:1089
#: ../src/LevelEditor.cpp:1186 ../src/LevelEditor.cpp:1250
#: ../src/LevelEditor.cpp:2929 ../src/LevelEditSelect.cpp:248
#: ../src/LevelEditSelect.cpp:281 ../src/LevelEditSelect.cpp:321
#: ../src/OptionsMenu.cpp:289
msgid "Cancel"
msgstr "Відміна"
#: ../src/Functions.cpp:1248
msgid "Abort"
msgstr "Перервати"
#: ../src/Functions.cpp:1249 ../src/Functions.cpp:1265
msgid "Retry"
msgstr "Спробувати знову"
#: ../src/Functions.cpp:1250
msgid "Ignore"
msgstr "Ігнорувати"
#: ../src/Functions.cpp:1254 ../src/Functions.cpp:1260
msgid "Yes"
msgstr "Так"
#: ../src/Functions.cpp:1255 ../src/Functions.cpp:1261
msgid "No"
msgstr "Ні"
# TRANSLATORS: Please do not remove %s or %d from your translation:
# - %d means the level number in a levelpack
# - %s means the name of current level
#: ../src/Game.cpp:280 ../src/Game.cpp:1236
#, c-format
msgid "Level %d %s"
msgstr "Рівень %d %s"
# TRANSLATORS: Please do not remove %s from your translation:
# - %s will be replaced with current action key
#: ../src/Game.cpp:915
#, c-format
msgid "Press %s key to save the game."
msgstr "Натисніть клавішу %s, щоб зберегти гру."
# TRANSLATORS: Please do not remove %s from your translation:
# - %s will be replaced with current action key
#: ../src/Game.cpp:920
#, c-format
msgid "Press %s key to swap the position of player and shadow."
msgstr "Натисніть клавішу %s, щоб поміняти гравця і тінь місцями."
# TRANSLATORS: Please do not remove %s from your translation:
# - %s will be replaced with current action key
#: ../src/Game.cpp:925
#, c-format
msgid "Press %s key to activate the switch."
msgstr "Натисніть %s, щоб увімкнути вимикач."
# TRANSLATORS: Please do not remove %s from your translation:
# - %s will be replaced with current action key
#: ../src/Game.cpp:930
#, c-format
msgid "Press %s key to teleport."
msgstr "Натисніть клавішу %s, щоб телепортуватися."
# TRANSLATORS: Please do not remove %s from your translation:
# - first %s means currently configured key to restart game
# - Second %s means configured key to load from last save
#: ../src/Game.cpp:972
#, c-format
msgid "Press %s to restart current level or press %s to load the game."
msgstr "Натисніть клавішу %s, щоб перезапустити поточний рівень, або натисніть клавішу %s, щоб завантажити гру."
# TRANSLATORS: Please do not remove %s from your translation:
# - %s will be replaced with currently configured key to restart game
#: ../src/Game.cpp:983
#, c-format
msgid "Press %s to restart current level."
msgstr "Натисніть клавішу %s, щоб перезапустити поточний рівень."
#: ../src/Game.cpp:996
msgid "Your shadow has died."
msgstr "Ваша тінь померла."
#: ../src/Game.cpp:1052
#, c-format
msgid "%d recording"
msgid_plural "%d recordings"
msgstr[0] "%d запис"
msgstr[1] "%d записи"
msgstr[2] "%d записів"
msgstr[3] "%d записів"
#: ../src/Game.cpp:1224
msgid "You've finished:"
msgstr "Ви завершили:"
# TRANSLATORS: Please do not remove %-.2f from your translation:
# - %-.2f means time in seconds
# - s is shortened form of a second. Try to keep it so.
#: ../src/Game.cpp:1291
#, c-format
msgid "Time: %-.2fs"
msgstr "Час: %-.2fs"
# TRANSLATORS: Please do not remove %-.2f from your translation:
# - %-.2f means time in seconds
# - s is shortened form of a second. Try to keep it so.
#: ../src/Game.cpp:1300
#, c-format
msgid "Best time: %-.2fs"
msgstr "Найкращий час: %-.2fs"
#: ../src/Game.cpp:1311
#, c-format
msgid "Target time: %-.2fs"
msgstr "Необхідний час: %-.2fs"
# TRANSLATORS: Please do not remove %d from your translation:
# - %d means the number of recordings user has made
#: ../src/Game.cpp:1332
#, c-format
msgid "Recordings: %d"
msgstr "Записів: %d"
# TRANSLATORS: Please do not remove %d from your translation:
# - %d means the number of recordings user has made
#: ../src/Game.cpp:1340
#, c-format
msgid "Best recordings: %d"
msgstr "Найкращі записи: %d"
#: ../src/Game.cpp:1350
#, c-format
msgid "Target recordings: %d"
msgstr "Необхідно записів: %d"
# TRANSLATORS: Please do not remove %s from your translation:
# - %s will be replaced with name of a prize medal (gold, silver or bronze)
#: ../src/Game.cpp:1363
#, c-format
msgid "You earned the %s medal"
msgstr "Ви отримали %s медаль"
#: ../src/Game.cpp:1363
msgid "GOLD"
msgstr "ЗОЛОТУ"
#: ../src/Game.cpp:1363
msgid "SILVER"
msgstr "СРІБНУ"
#: ../src/Game.cpp:1363
msgid "BRONZE"
msgstr "БРОНЗОВУ"
# TRANSLATORS: used as return to the level selector menu
#: ../src/Game.cpp:1390
msgid "Menu"
msgstr "Меню"
# TRANSLATORS: used as restart level
#: ../src/Game.cpp:1397 ../src/InputManager.cpp:47
msgid "Restart"
msgstr "Перезапустити"
# TRANSLATORS: used as next level
#: ../src/Game.cpp:1404
msgid "Next"
msgstr "Далі"
#: ../src/Game.cpp:1430
msgid "Game replay is done."
msgstr "Відтворення завершене."
#: ../src/Game.cpp:1430
msgid "Game Replay"
msgstr "Відтворення гри"
#: ../src/Game.cpp:1767 ../src/Game.cpp:1769
msgid "Congratulations"
msgstr "Вітання"
#: ../src/Game.cpp:1769
msgid "You have finished the levelpack!"
msgstr "Ви закінчили цю збірку рівнів!"
#: ../src/InputManager.cpp:46
msgid "Up (in menu)"
msgstr "Вгору (в меню)"
#: ../src/InputManager.cpp:46
msgid "Down (in menu)"
msgstr "Вниз (в меню)"
#: ../src/InputManager.cpp:46
msgid "Left"
msgstr "Ліворуч"
#: ../src/InputManager.cpp:46
msgid "Right"
msgstr "Праворуч"
#: ../src/InputManager.cpp:46
msgid "Jump"
msgstr "Стрибок"
#: ../src/InputManager.cpp:46
msgid "Action"
msgstr "Дія"
#: ../src/InputManager.cpp:46
msgid "Space (Record)"
msgstr "Пробіл (Запис)"
#: ../src/InputManager.cpp:46
msgid "Cancel recording"
msgstr "Відмінити запис"
#: ../src/InputManager.cpp:47
msgid "Escape"
msgstr "Вихід"
#: ../src/InputManager.cpp:47
msgid "Tab (View shadow/Level prop.)"
msgstr "Tab (Див. тінь/Налашт. рівня)"
#: ../src/InputManager.cpp:47
msgid "Save game (in editor)"
msgstr "Зберегти гру (в редакторі)"
#: ../src/InputManager.cpp:47
msgid "Load game"
msgstr "Завантажити гру"
#: ../src/InputManager.cpp:47
msgid "Swap (in editor)"
msgstr "Помінятися (в редакторі)"
#: ../src/InputManager.cpp:48
msgid "Teleport (in editor)"
msgstr "Телепортація (в редакторі)"
#: ../src/InputManager.cpp:48
msgid "Suicide (in editor)"
msgstr "Самогубство (в редакторі)"
#: ../src/InputManager.cpp:48
msgid "Shift (in editor)"
msgstr "Зсув (в редакторі)"
#: ../src/InputManager.cpp:48
msgid "Next block type (in Editor)"
msgstr "Наступний тип блока (в редакторі)"
#: ../src/InputManager.cpp:49
msgid "Previous block type (in editor)"
msgstr "Попередній тип блока (в редакторі)"
#: ../src/InputManager.cpp:49
msgid "Select (in menu)"
msgstr "Обрати (в меню)"
# TRANSLAOTRS: This is used when the name of the key code is not found.
#: ../src/InputManager.cpp:156
#, c-format
msgid "(Key %d)"
msgstr "(Клавіша %d)"
#: ../src/InputManager.cpp:163
#, c-format
msgid "Joystick axis %d %s"
msgstr "Вісь джойстика %d %s"
#: ../src/InputManager.cpp:166
#, c-format
msgid "Joystick button %d"
msgstr "Кнопка джойстика %d"
#: ../src/InputManager.cpp:171
#, c-format
msgid "Joystick hat %d left"
msgstr "Джойстик %d ліворуч"
#: ../src/InputManager.cpp:174
#, c-format
msgid "Joystick hat %d right"
msgstr "Джойстик %d праворуч"
#: ../src/InputManager.cpp:177
#, c-format
msgid "Joystick hat %d up"
msgstr "Джойстик %d вгору"
#: ../src/InputManager.cpp:180
#, c-format
msgid "Joystick hat %d down"
msgstr "Джойстик %d вниз"
# TRANSLAOTRS: This is used when the JOYSTICK_HAT value is invalid.
#: ../src/InputManager.cpp:185
#, c-format
msgid "Joystick hat %d %d"
msgstr "Джойстик %d %d"
#: ../src/InputManager.cpp:202
msgid "OR"
msgstr "АБО"
#: ../src/InputManager.cpp:416
msgid "Select an item and press a key to change it."
msgstr "Оберіть елемент і натисніть клавішу, щоб змінити його."
#: ../src/InputManager.cpp:419
msgid "Press backspace to clear the selected item."
msgstr "Натисніть backspace, щоб очистити обраний елемент."
#: ../src/LevelEditor.cpp:56
msgid "Block"
msgstr "Блок"
#: ../src/LevelEditor.cpp:56
msgid "Player Start"
msgstr "Тут починає гравець"
#: ../src/LevelEditor.cpp:56
msgid "Shadow Start"
msgstr "Тут починає тінь"
#: ../src/LevelEditor.cpp:57
msgid "Exit"
msgstr "Вихід"
#: ../src/LevelEditor.cpp:57
msgid "Shadow Block"
msgstr "Блок для тіні"
#: ../src/LevelEditor.cpp:57
msgid "Spikes"
msgstr "Шипи"
#: ../src/LevelEditor.cpp:58
msgid "Checkpoint"
msgstr "Чекпоінт"
#: ../src/LevelEditor.cpp:58 ../src/LevelEditSelect.cpp:312
msgid "Swap"
msgstr "Помінятися"
#: ../src/LevelEditor.cpp:58
msgid "Fragile"
msgstr "Крихкий"
#: ../src/LevelEditor.cpp:59
msgid "Moving Block"
msgstr "Рухомий блок"
#: ../src/LevelEditor.cpp:59
msgid "Moving Shadow Block"
msgstr "Рухомий блок для тіні"
#: ../src/LevelEditor.cpp:59
msgid "Moving Spikes"
msgstr "Рухомі шипи"
#: ../src/LevelEditor.cpp:60
msgid "Teleporter"
msgstr "Телепортер"
#: ../src/LevelEditor.cpp:60
msgid "Button"
msgstr "Кнопка"
#: ../src/LevelEditor.cpp:60
msgid "Switch"
msgstr "Вимикач"
#: ../src/LevelEditor.cpp:61
msgid "Conveyor Belt"
msgstr "Конвеєр"
#: ../src/LevelEditor.cpp:61
msgid "Shadow Conveyor Belt"
msgstr "Конвеєр для тіні"
#: ../src/LevelEditor.cpp:61
msgid "Notification Block"
msgstr "Інформаційний блок"
#: ../src/LevelEditor.cpp:61
msgid "Collectable"
msgstr "Колекційний предмет"
#: ../src/LevelEditor.cpp:61
msgid "Pushable"
msgstr "Блок, що можна штовхати"
#: ../src/LevelEditor.cpp:65 ../src/LevelEditor.cpp:310
msgid "Select"
msgstr "Обрати"
#: ../src/LevelEditor.cpp:65
msgid "Add"
msgstr "Додати"
#: ../src/LevelEditor.cpp:65 ../src/LevelEditor.cpp:311
msgid "Delete"
msgstr "Видалити"
#: ../src/LevelEditor.cpp:65 ../src/LevelPlaySelect.cpp:66
#: ../src/TitleMenu.cpp:43
msgid "Play"
msgstr "Грати"
#: ../src/LevelEditor.cpp:65 ../src/LevelEditor.cpp:2852
msgid "Level settings"
msgstr "Налаштування рівня"
#: ../src/LevelEditor.cpp:65
msgid "Save level"
msgstr "Зберегти рівень"
#: ../src/LevelEditor.cpp:65
msgid "Back to menu"
msgstr "Назад до меню"
#: ../src/LevelEditor.cpp:65
msgid "Configure"
msgstr "Налаштування"
#: ../src/LevelEditor.cpp:84
#, c-format
msgid "%s (Scenery)"
msgstr "%s (Тло)"
#: ../src/LevelEditor.cpp:267
msgid "Toggle"
msgstr "Перемикач"
#: ../src/LevelEditor.cpp:270
msgid "Complete"
msgstr "Завершений"
#: ../src/LevelEditor.cpp:271
msgid "One step"
msgstr "Один крок"
#: ../src/LevelEditor.cpp:272
msgid "Two steps"
msgstr "Два кроки"
#: ../src/LevelEditor.cpp:273
msgid "Gone"
msgstr "Зник"
#: ../src/LevelEditor.cpp:291
msgid "Negative infinity"
msgstr "Мінус нескінченність"
#: ../src/LevelEditor.cpp:293
msgid "Zero"
msgstr "Нуль"
#: ../src/LevelEditor.cpp:295
msgid "Level size"
msgstr "Розмір рівня"
#: ../src/LevelEditor.cpp:297
msgid "Positive infinity"
msgstr "Плюс нескінченність"
#: ../src/LevelEditor.cpp:299
msgid "Default"
msgstr "За замовчуванням"
#: ../src/LevelEditor.cpp:308
msgid "Deselect"
msgstr "Зняти вибір"
#: ../src/LevelEditor.cpp:318 ../src/LevelEditor.cpp:1136
#, c-format
msgid "Horizontal repeat start: %s"
msgstr "Розпочати горизонтальний повтор: %s"
#: ../src/LevelEditor.cpp:320 ../src/LevelEditor.cpp:1137
#, c-format
msgid "Horizontal repeat end: %s"
msgstr "Завершити горизонтальний повтор: %s"
#: ../src/LevelEditor.cpp:322 ../src/LevelEditor.cpp:1138
#, c-format
msgid "Vertical repeat start: %s"
msgstr "Розпочати вертикальний повтор: %s"
#: ../src/LevelEditor.cpp:324 ../src/LevelEditor.cpp:1139
#, c-format
msgid "Vertical repeat end: %s"
msgstr "Завершити вертикальний повтор: %s"
#: ../src/LevelEditor.cpp:329 ../src/LevelEditor.cpp:1150
msgid "Custom scenery"
msgstr "Налаштувати тло"
#: ../src/LevelEditor.cpp:335 ../src/LevelEditor.cpp:600
#: ../src/LevelEditor.cpp:602
msgid "Visible"
msgstr "Видимий"
#: ../src/LevelEditor.cpp:344
msgid "Link"
msgstr "Посилання"
#: ../src/LevelEditor.cpp:345
msgid "Remove Links"
msgstr "Прибрати посилання"
#: ../src/LevelEditor.cpp:349 ../src/LevelEditor.cpp:624
#: ../src/LevelEditor.cpp:626
msgid "Automatic"
msgstr "Авто"
#: ../src/LevelEditor.cpp:359 ../src/LevelEditor.cpp:649
#, c-format
msgid "Behavior: %s"
msgstr "Поведінка: %s"
#: ../src/LevelEditor.cpp:362
msgid "Path"
msgstr "Шлях"
#: ../src/LevelEditor.cpp:363
msgid "Remove Path"
msgstr "Прибрати шлях"
#: ../src/LevelEditor.cpp:365 ../src/LevelEditor.cpp:371
#: ../src/LevelEditor.cpp:587 ../src/LevelEditor.cpp:589
msgid "Activated"
msgstr "Активовано"
#: ../src/LevelEditor.cpp:366 ../src/LevelEditor.cpp:612
#: ../src/LevelEditor.cpp:614
msgid "Looping"
msgstr "Повторювати"
#: ../src/LevelEditor.cpp:372 ../src/LevelEditor.cpp:3526
msgid "Speed"
msgstr "Швидкість"
#: ../src/LevelEditor.cpp:378 ../src/LevelEditor.cpp:668
#, c-format
msgid "State: %s"
msgstr "Стан: %s"
#: ../src/LevelEditor.cpp:382 ../src/LevelEditor.cpp:3511
msgid "Message"
msgstr "Повідомлення"
#: ../src/LevelEditor.cpp:384 ../src/LevelEditor.cpp:1202
#: ../src/LevelEditor.cpp:3825
msgid "Appearance"
msgstr "Зовнішній вигляд"
#: ../src/LevelEditor.cpp:389 ../src/LevelEditor.cpp:431
#: ../src/LevelEditor.cpp:715
msgid "Scripting"
msgstr "Скрипти"
#: ../src/LevelEditor.cpp:402 ../src/LevelEditor.cpp:867
#: ../src/LevelEditor.cpp:885
#, c-format
msgid "Background layer: %s"
msgstr "Шар тла: %s"
#: ../src/LevelEditor.cpp:409 ../src/LevelEditor.cpp:866
#: ../src/LevelEditor.cpp:884
msgid "Blocks layer"
msgstr "Шар блоків"
#: ../src/LevelEditor.cpp:417 ../src/LevelEditor.cpp:867
#: ../src/LevelEditor.cpp:885
#, c-format
msgid "Foreground layer: %s"
msgstr "Шар переднього плану: %s"
#: ../src/LevelEditor.cpp:423
msgid "Add new layer"
msgstr "Додати новий шар"
#: ../src/LevelEditor.cpp:424
msgid "Delete selected layer"
msgstr "Видалити обрані шари"
#: ../src/LevelEditor.cpp:425
msgid "Configure selected layer"
msgstr "Налаштувати обраний шар"
#: ../src/LevelEditor.cpp:426
msgid "Move selected object to layer"
msgstr "Перемістити обраний об'єкт у шар"
#: ../src/LevelEditor.cpp:430 ../src/OptionsMenu.cpp:55
msgid "Settings"
msgstr "Налаштування"
#: ../src/LevelEditor.cpp:463
msgid ""
"NOTE: the layers are sorted by name alphabetically.\n"
"The layer is background layer if its name is < 'f'\n"
"by dictionary order, otherwise it's foreground layer."
msgstr "ПРИМІТКА: шари сортуються за назвою у алфавітному порядку.\n"
"Шар є тлом, якщо його ім'я < 'f'\n"
"у словниковому порядку, інакше - це шар переднього плану."
#: ../src/LevelEditor.cpp:539
msgid "Notification block"
msgstr "Інформаційний блок"
#: ../src/LevelEditor.cpp:545
msgid "Enter message here:"
msgstr "Введіть повідомлення тут:"
#: ../src/LevelEditor.cpp:646
msgid "Behavior"
msgstr "Поведінка"
#: ../src/LevelEditor.cpp:665
msgid "State"
msgstr "Стан"
#: ../src/LevelEditor.cpp:673
msgid "Conveyor belt speed"
msgstr "Швидкість конвеєра"
#: ../src/LevelEditor.cpp:679
msgid "Enter speed here:"
msgstr "Введіть швидкість:"
#: ../src/LevelEditor.cpp:690
msgid "NOTE: 1 Speed = 0.08 block/s"
msgstr "ПРИМ.: 1 Швидкість = 0,08 блоків/с"
#: ../src/LevelEditor.cpp:721
msgid "Id:"
msgstr "Ід:"
#: ../src/LevelEditor.cpp:787
msgid "Level Scripting"
msgstr "Скрипти рівня"
#: ../src/LevelEditor.cpp:892
msgid "Add layer"
msgstr "Додати шар"
#: ../src/LevelEditor.cpp:898
msgid "Enter the layer name:"
msgstr "Введіть назву шару:"
#: ../src/LevelEditor.cpp:943
#, c-format
msgid "Are you sure you want to delete layer '%s'?"
msgstr "Ви впевнені, що хочете видалити шар '%s'?"
#: ../src/LevelEditor.cpp:944
msgid "Delete layer"
msgstr "Видалити шар"
#: ../src/LevelEditor.cpp:968
msgid "Layer settings"
msgstr "Налаштувати шар"
#: ../src/LevelEditor.cpp:974
msgid "Layer name:"
msgstr "Назва шару:"
#: ../src/LevelEditor.cpp:989
msgid "Layer moving speed (1 speed = 0.8 block/s):"
msgstr "Швидкість переміщення шару (1 швидкість = 0,08 блоків/с):"
#: ../src/LevelEditor.cpp:1010
msgid "Speed of following camera:"
msgstr "Швидкість камери:"
#: ../src/LevelEditor.cpp:1062
msgid "Move to layer"
msgstr "Перемістити у шар"
#: ../src/LevelEditor.cpp:1068
msgid "Enter the layer name (create new layer if necessary):"
msgstr "Введіть назву шару (створити новий шар за необхідності):"
#: ../src/LevelEditor.cpp:1132
msgid "Repeat mode"
msgstr "Режим повтору"
#: ../src/LevelEditor.cpp:1156
msgid "Custom scenery:"
msgstr "Тло:"
#: ../src/LevelEditor.cpp:1219
msgid "(Use the default appearance for this block)"
msgstr "(Використовувати звичайний вигляд цього блока)"
# TRANSLATORS: Block name
# TRANSLATORS: Context: Resize/Move ...
# TRANSLATORS: Context: Add/Remove ...
# TRANSLATORS: Context: Undo/Redo ...
#: ../src/LevelEditor.cpp:1465 ../src/LevelEditor.cpp:1707
#: ../src/LevelEditor.cpp:1723 ../src/LevelEditor.cpp:1772
#: ../src/LevelEditor.cpp:4400
msgid "Custom scenery block"
msgstr "Блок тла"
#: ../src/LevelEditor.cpp:1673
msgid "Toolbox"
msgstr "Інструменти"
#: ../src/LevelEditor.cpp:1705
#, c-format
msgid "Resize %s"
msgstr "Змінити розмір %s"
#: ../src/LevelEditor.cpp:1705
#, c-format
msgid "Move %s"
msgstr "Перемістити %s"
# TRANSLATORS: Context: Undo/Redo ...
#: ../src/LevelEditor.cpp:1713
#, c-format
msgid "Move %d object"
msgid_plural "Move %d objects"
msgstr[0] "Перемістити %d об'єкт"
msgstr[1] "Перемістити %d об'єкти"
msgstr[2] "Перемістити %d об'єктів"
msgstr[3] "Перемістити %d об'єктів"
#: ../src/LevelEditor.cpp:1721
#, c-format
msgid "Add %s"
msgstr "Додати %s"
#: ../src/LevelEditor.cpp:1721
#, c-format
msgid "Remove %s"
msgstr "Видалити %s"
# TRANSLATORS: Context: Undo/Redo ...
#: ../src/LevelEditor.cpp:1729
#, c-format
msgid "Add %d object"
msgid_plural "Add %d objects"
msgstr[0] "Додати %d об'єкт"
msgstr[1] "Додати %d об'єкти"
msgstr[2] "Додати %d об'єктів"
msgstr[3] "Додати %d об'єктів"
# TRANSLATORS: Context: Undo/Redo ...
#: ../src/LevelEditor.cpp:1731
#, c-format
msgid "Remove %d object"
msgid_plural "Remove %d objects"
msgstr[0] "Видалити %d об'єкт"
msgstr[1] "Видалити %d об'єкти"
msgstr[2] "Видалити %d об'єктів"
msgstr[3] "Видалити %d об'єктів"
# TRANSLATORS: Context: Undo/Redo ...
#: ../src/LevelEditor.cpp:1739
#, c-format
msgid "Add path to %s"
msgstr "Додати шлях до %s"
# TRANSLATORS: Context: Undo/Redo ...
#: ../src/LevelEditor.cpp:1741
#, c-format
msgid "Remove a path point from %s"
msgstr "Видалити точку шляху з %s"
# TRANSLATORS: Context: Undo/Redo ...
#: ../src/LevelEditor.cpp:1747
#, c-format
msgid "Remove all paths from %s"
msgstr "Видалити усі шляхи з %s"
# TRANSLATORS: Context: Undo/Redo ...
#: ../src/LevelEditor.cpp:1753
#, c-format
msgid "Add link from %s to %s"
msgstr "Додати зв'язок між %s і %s"
# TRANSLATORS: Context: Undo/Redo ...
#: ../src/LevelEditor.cpp:1759
#, c-format
msgid "Remove all links from %s"
msgstr "Видалити усі зв'язки з %s"
# TRANSLATORS: Context: Undo/Redo ...
#: ../src/LevelEditor.cpp:1766
msgid "Modify the %2 property of %1"
msgstr "Змінити у %1 властивість %2"
# TRANSLATORS: Context: Undo/Redo ...
#: ../src/LevelEditor.cpp:1818
#, c-format
msgid "Edit the script of %s"
msgstr "Редагувати скрипт %s"
# TRANSLATORS: Context: Undo/Redo ...
#: ../src/LevelEditor.cpp:1821
msgid "Edit the script of level"
msgstr "Редагувати скрипт рівня"
#: ../src/LevelEditor.cpp:2146 ../src/LevelEditor.cpp:2226
msgid "The level has unsaved changes."
msgstr "На рівні є незбережені зміни."
#: ../src/LevelEditor.cpp:2150 ../src/LevelEditor.cpp:2228
msgid "Are you sure you want to quit?"
msgstr "Ви дійсно хочете вийти?"
#: ../src/LevelEditor.cpp:2150 ../src/LevelEditor.cpp:2228
msgid "Quit prompt"
msgstr "Вихід"
#: ../src/LevelEditor.cpp:2797 ../src/LevelEditor.cpp:2799
#, c-format
msgid "Level \"%s\" saved"
msgstr "Рівень \"%s\" збережено"
#: ../src/LevelEditor.cpp:2797 ../src/LevelEditor.cpp:2799
msgid "Saved"
msgstr "Збережено"
#: ../src/LevelEditor.cpp:2859 ../src/LevelEditSelect.cpp:208
msgid "Name:"
msgstr "Назва:"
#: ../src/LevelEditor.cpp:2866
msgid "Theme:"
msgstr "Оформлення:"
#: ../src/LevelEditor.cpp:2873
msgid "Examples: %DATA%/themes/classic"
msgstr "Приклади: %DATA%/themes/classic"
#: ../src/LevelEditor.cpp:2875
msgid "or %USER%/themes/Orange"
msgstr "або %USER%/themes/Orange"
#: ../src/LevelEditor.cpp:2878
msgid "Music:"
msgstr "Музика:"
#: ../src/LevelEditor.cpp:2887
msgid "Target time (s):"
msgstr "Необхідний час (с):"
#: ../src/LevelEditor.cpp:2903
msgid "Target recordings:"
msgstr "Необхідно записів:"
#: ../src/LevelEditor.cpp:2919
msgid "Restart level editor is required"
msgstr "Необхідно перезавантажити редактор рівнів"
#: ../src/LevelEditor.cpp:3679 ../src/LevelEditor.cpp:3718
#: ../src/LevelEditor.cpp:3739
msgid "Please enter a layer name."
msgstr "Будь ласка, введіть назву шару."
#: ../src/LevelEditor.cpp:3679 ../src/LevelEditor.cpp:3683
#: ../src/LevelEditor.cpp:3718 ../src/LevelEditor.cpp:3722
#: ../src/LevelEditor.cpp:3739 ../src/LevelEditor.cpp:3743
#: ../src/LevelEditSelect.cpp:644 ../src/LevelEditSelect.cpp:683
#: ../src/LevelEditSelect.cpp:688 ../src/LevelEditSelect.cpp:693
#: ../src/LevelEditSelect.cpp:698 ../src/LevelEditSelect.cpp:796
msgid "Error"
msgstr "Помилка"
#: ../src/LevelEditor.cpp:3683 ../src/LevelEditor.cpp:3722
#, c-format
msgid "The layer '%s' already exists."
msgstr "Шар '%s' вже існує."
#: ../src/LevelEditor.cpp:3743
msgid "Source and destination layers are the same."
msgstr "Вихідний та кінцевий шари співпадають."
#: ../src/LevelEditor.cpp:3760
msgid "Scenery"
msgstr "Тло"
#: ../src/LevelEditor.cpp:4190 ../src/LevelEditor.cpp:4218
#, c-format
msgid "Speed: %d = %0.2f block/s"
msgstr "Швидкість: %d = %0.2f блоків/с"
#: ../src/LevelEditor.cpp:4203
msgid "Stop at this point"
msgstr "Зупинитися у цій точці"
#: ../src/LevelEditor.cpp:4208
#, c-format
msgid "Pause: %d = %0.3fs"
msgstr "Пауза: %d = %0.3fs"
#: ../src/LevelEditSelect.cpp:41 ../src/TitleMenu.cpp:45
msgid "Map Editor"
msgstr "Редактор карт"
#: ../src/LevelEditSelect.cpp:66
msgid "New Levelpack"
msgstr "Нова збірка рівнів"
#: ../src/LevelEditSelect.cpp:71
msgid "Pack Properties"
msgstr "Властивості збірки"
#: ../src/LevelEditSelect.cpp:76
msgid "Remove Pack"
msgstr "Видалити збірку"
#: ../src/LevelEditSelect.cpp:81
msgid "Move Map"
msgstr "Перемістити карту"
#: ../src/LevelEditSelect.cpp:89
msgid "Remove Map"
msgstr "Видалити карту"
#: ../src/LevelEditSelect.cpp:94
msgid "Edit Map"
msgstr "Редагувати карту"
#: ../src/LevelEditSelect.cpp:205
msgid "Properties"
msgstr "Властивості"
#: ../src/LevelEditSelect.cpp:217
msgid "Description:"
msgstr "Опис:"
#: ../src/LevelEditSelect.cpp:226
msgid "Congratulation text:"
msgstr "Текст привітання:"
#: ../src/LevelEditSelect.cpp:235
msgid "Music list:"
msgstr "Перелік музики:"
#: ../src/LevelEditSelect.cpp:265 ../src/LevelEditSelect.cpp:485
msgid "Add level"
msgstr "Додати рівень"
#: ../src/LevelEditSelect.cpp:268
msgid "File name:"
msgstr "Назва файлу:"
#: ../src/LevelEditSelect.cpp:293
msgid "Move level"
msgstr "Перемістити рівень"
#: ../src/LevelEditSelect.cpp:296
msgid "Level: "
msgstr "Рівень: "
#: ../src/LevelEditSelect.cpp:310
msgid "Before"
msgstr "До"
#: ../src/LevelEditSelect.cpp:311
msgid "After"
msgstr "Після"
#: ../src/LevelEditSelect.cpp:368 ../src/LevelPlaySelect.cpp:124
msgid "Individual levels which are not contained in any level packs"
msgstr "Окремі рівні, що не включено до жодної збірки рівнів"
#: ../src/LevelEditSelect.cpp:577
#, c-format
msgid "Are you sure remove the level pack '%s'?"
msgstr "Ви впевнені, що хочете видалити збірку рівнів '%s'?"
#: ../src/LevelEditSelect.cpp:577 ../src/LevelEditSelect.cpp:607
msgid "Remove prompt"
msgstr "Видалення"
#: ../src/LevelEditSelect.cpp:607
#, c-format
msgid "Are you sure remove the map '%s'?"
msgstr "Ви впевнені, що хочете видалити карту '%s'?"
#: ../src/LevelEditSelect.cpp:644
msgid "Levelpack name cannot be empty."
msgstr "Назва збірки рівнів не може бути порожньою."
#: ../src/LevelEditSelect.cpp:683
#, c-format
msgid "The levelpack directory '%s' already exists!"
msgstr "Директорія збірки рівнів '%s' вже існує!"
#: ../src/LevelEditSelect.cpp:688
#, c-format
msgid "Unable to create levelpack directory '%s'!"
msgstr "Неможливо створити директорію збірки рівнів '%s'!"
#: ../src/LevelEditSelect.cpp:693
#, c-format
msgid "The levelpack file '%s' already exists!"
msgstr "Файл збірки рівнів '%s' вже існує!"
#: ../src/LevelEditSelect.cpp:698
#, c-format
msgid "Unable to create levelpack file '%s'!"
msgstr "Неможливо створити файл збірки рівнів '%s'!"
#: ../src/LevelEditSelect.cpp:758
msgid "No file name given for the new level."
msgstr "Новому рівню не було призначене ім'я файлу."
#: ../src/LevelEditSelect.cpp:758
msgid "Missing file name"
msgstr "Відсутнє ім'я файлу"
#: ../src/LevelEditSelect.cpp:796
#, c-format
msgid "The file %s already exists."
msgstr "Файл %s вже існує."
#: ../src/LevelEditSelect.cpp:849
msgid "The entered level number isn't valid!"
msgstr "Введений номер рівня невірний!"
#: ../src/LevelEditSelect.cpp:849
msgid "Illegal number"
msgstr "Невірний номер"
#: ../src/LevelInfoRender.cpp:19
msgid "Choose a level"
msgstr "Оберіть рівень"
#: ../src/LevelInfoRender.cpp:20
msgid "Time:"
msgstr "Час:"
#: ../src/LevelInfoRender.cpp:21 ../src/StatisticsScreen.cpp:259
msgid "Recordings:"
msgstr "Записів:"
#: ../src/LevelPackManager.cpp:124
msgid "Custom Levels"
msgstr "Інші рівні"
#: ../src/LevelPlaySelect.cpp:41
msgid "Select Level"
msgstr "Оберіть рівень"
# TRANSLATORS: Used for button which clear any level progress like unlocked
# levels and highscores.
#: ../src/OptionsMenu.cpp:66
msgid "Clear Progress"
msgstr "Скинути прогрес"
#: ../src/OptionsMenu.cpp:109
msgid "General"
msgstr "Загальні"
#: ../src/OptionsMenu.cpp:110
msgid "Controls"
msgstr "Керування"
#: ../src/OptionsMenu.cpp:121
msgid "Music"
msgstr "Музика"
#: ../src/OptionsMenu.cpp:129
msgid "Sound"
msgstr "Звуки"
#: ../src/OptionsMenu.cpp:137
msgid "Resolution"
msgstr "Роздільна здатність"
#: ../src/OptionsMenu.cpp:177
msgid "Language"
msgstr "Мова"
# TRANSLATORS: as detect user's language automatically
#: ../src/OptionsMenu.cpp:185
msgid "Auto-Detect"
msgstr "Автовизначення"
#: ../src/OptionsMenu.cpp:209
msgid "Theme"
msgstr "Оформлення"
#: ../src/OptionsMenu.cpp:247
msgid "Internet proxy"
msgstr "Інтернет-проксі"
#: ../src/OptionsMenu.cpp:256
msgid "Fullscreen"
msgstr "На весь екран"
#: ../src/OptionsMenu.cpp:261
msgid "Quick record"
msgstr "Швидкий запис"
#: ../src/OptionsMenu.cpp:266
msgid "Internet"
msgstr "Інтернет"
#: ../src/OptionsMenu.cpp:271
msgid "Fade transition"
msgstr "Плавний перехід"
#: ../src/OptionsMenu.cpp:294
msgid "Save Changes"
msgstr "Зберегти зміни"
#: ../src/OptionsMenu.cpp:513
msgid "Do you really want to reset level progress?"
msgstr "Ви дійсно хочете скинути прогрес рівня?"
#: ../src/OptionsMenu.cpp:513
msgid "Warning"
msgstr "Попередження"
#: ../src/StatisticsManager.cpp:386
msgid "New achievement:"
msgstr "Нове досягнення:"
#: ../src/StatisticsManager.cpp:394
#, c-format
msgid "Achieved on %s"
msgstr "Досягнуто %s"
#: ../src/StatisticsManager.cpp:400
msgid "Unknown achievement"
msgstr "Невідоме досягнення"
#: ../src/StatisticsManager.cpp:406
#, c-format
msgid "Achieved %1.0f%%"
msgstr "Досягнуто %1.0f%%"
#: ../src/StatisticsManager.cpp:410
msgid "Not achieved"
msgstr "Не досягнуто"
#: ../src/StatisticsScreen.cpp:57 ../src/TitleMenu.cpp:55
msgid "Achievements and Statistics"
msgstr "Досягнення і статистика"
#: ../src/StatisticsScreen.cpp:166
msgid "Achievements"
msgstr "Досягнення"
#: ../src/StatisticsScreen.cpp:167
msgid "Statistics"
msgstr "Статистика"
#: ../src/StatisticsScreen.cpp:234
msgid "Total"
msgstr "Загалом"
#: ../src/StatisticsScreen.cpp:246
msgid "Traveling distance (m)"
msgstr "Пройдена відстань (м)"
#: ../src/StatisticsScreen.cpp:247
msgid "Jump times"
msgstr "Стрибків"
#: ../src/StatisticsScreen.cpp:248
msgid "Die times"
msgstr "Смертей"
#: ../src/StatisticsScreen.cpp:249
msgid "Squashed times"
msgstr "Разів розчавлено"
#: ../src/StatisticsScreen.cpp:260
msgid "Switch pulled times:"
msgstr "Увімкнено вимикачів:"
#: ../src/StatisticsScreen.cpp:261
msgid "Swap times:"
msgstr "Змін:"
#: ../src/StatisticsScreen.cpp:262
msgid "Save times:"
msgstr "Збережень:"
#: ../src/StatisticsScreen.cpp:263
msgid "Load times:"
msgstr "Завантажень:"
#: ../src/StatisticsScreen.cpp:268
msgid "Completed levels:"
msgstr "Завершених рівнів:"
#: ../src/StatisticsScreen.cpp:306
msgid "In-game time:"
msgstr "Час у грі:"
#: ../src/StatisticsScreen.cpp:308
msgid "Level editing time:"
msgstr "Час у редакторі:"
#: ../src/StatisticsScreen.cpp:310
msgid "Created levels:"
msgstr "Створено рівнів:"
#: ../src/TitleMenu.cpp:44
msgid "Options"
msgstr "Налаштування"
#: ../src/TitleMenu.cpp:47
msgid "Quit"
msgstr "Вихід"
#: ../src/TitleMenu.cpp:131
msgid "Enable internet in order to install addons."
msgstr "Щоб встановлювати застосунки, дозвольте підключення до Інтернету."
#: ../src/TitleMenu.cpp:131
msgid "Internet disabled"
msgstr "Підключення до інтернету вимкнуто"
# TRANSLATORS: name of a key
msgctxt "keys"
msgid "Return"
msgstr "Return"
# TRANSLATORS: name of a key
msgctxt "keys"
msgid "Escape"
msgstr "Escape"
# TRANSLATORS: name of a key
msgctxt "keys"
msgid "Backspace"
msgstr "Backspace"
# TRANSLATORS: name of a key
msgctxt "keys"
msgid "Tab"
msgstr "Tab"
# TRANSLATORS: name of a key
msgctxt "keys"
msgid "Space"
msgstr "Пробіл"
# TRANSLATORS: name of a key
msgctxt "keys"
msgid "CapsLock"
msgstr "CapsLock"
# TRANSLATORS: name of a key
msgctxt "keys"
msgid "PrintScreen"
msgstr "PrintScreen"
# TRANSLATORS: name of a key
msgctxt "keys"
msgid "ScrollLock"
msgstr "ScrollLock"
# TRANSLATORS: name of a key
msgctxt "keys"
msgid "Pause"
msgstr "Pause"
# TRANSLATORS: name of a key
msgctxt "keys"
msgid "Insert"
msgstr "Insert"
# TRANSLATORS: name of a key
msgctxt "keys"
msgid "Home"
msgstr "Home"
# TRANSLATORS: name of a key
msgctxt "keys"
msgid "PageUp"
msgstr "PageUp"
# TRANSLATORS: name of a key
msgctxt "keys"
msgid "Delete"
msgstr "Delete"
# TRANSLATORS: name of a key
msgctxt "keys"
msgid "End"
msgstr "End"
# TRANSLATORS: name of a key
msgctxt "keys"
msgid "PageDown"
msgstr "PageDown"
# TRANSLATORS: name of a key
msgctxt "keys"
msgid "Right"
msgstr "Праворуч"
# TRANSLATORS: name of a key
msgctxt "keys"
msgid "Left"
msgstr "Ліворуч"
# TRANSLATORS: name of a key
msgctxt "keys"
msgid "Down"
msgstr "Вниз"
# TRANSLATORS: name of a key
msgctxt "keys"
msgid "Up"
msgstr "Угору"
# TRANSLATORS: name of a key
msgctxt "keys"
msgid "Numlock"
msgstr "Numlock"
# TRANSLATORS: name of a key
msgctxt "keys"
msgid "SysReq"
msgstr "SysReq"
# TRANSLATORS: name of a key
msgctxt "keys"
msgid "Left Ctrl"
msgstr "Лівий Ctrl"
# TRANSLATORS: name of a key
msgctxt "keys"
msgid "Left Shift"
msgstr "Лівий Shift"
# TRANSLATORS: name of a key
msgctxt "keys"
msgid "Left Alt"
msgstr "Лівий Alt"
# TRANSLATORS: name of a key
msgctxt "keys"
msgid "Left GUI"
msgstr "Лівий GUI"
# TRANSLATORS: name of a key
msgctxt "keys"
msgid "Right Ctrl"
msgstr "Правий Ctrl"
# TRANSLATORS: name of a key
msgctxt "keys"
msgid "Right Shift"
msgstr "Правий Shift"
# TRANSLATORS: name of a key
msgctxt "keys"
msgid "Right Alt"
msgstr "Правий Alt"
# TRANSLATORS: name of a key
msgctxt "keys"
msgid "Right GUI"
msgstr "Правий GUI"
diff --git a/src/Addons.cpp b/src/Addons.cpp
index 8d98c9a..2fa5270 100644
--- a/src/Addons.cpp
+++ b/src/Addons.cpp
@@ -1,1082 +1,1098 @@
/*
* Copyright (C) 2011-2013 Me and My Shadow
*
* This file is part of Me and My Shadow.
*
* Me and My Shadow is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Me and My Shadow is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Me and My Shadow. If not, see <http://www.gnu.org/licenses/>.
*/
#include "Addons.h"
#include "GameState.h"
#include "Functions.h"
#include "FileManager.h"
#include "GUIObject.h"
#include "GUIOverlay.h"
#include "GUIScrollBar.h"
#include "GUITextArea.h"
#include "GUIListBox.h"
#include "POASerializer.h"
#include "LevelPackManager.h"
#include "InputManager.h"
#include "ThemeManager.h"
#include <string>
#include <sstream>
#include <iostream>
#include "libs/tinyformat/tinyformat.h"
#include <SDL.h>
#include <SDL_ttf.h>
using namespace std;
static const char* predefinedCategories[] = {
"levels", __("Levels"), __("Single level which usually contain demanding puzzles"),
"levelpacks", __("Levelpacks"), __("Collection of levels with the same author or style"),
"themes", __("Themes"), __("Give every block and background a new look and feel"),
NULL,
};
static std::map<std::string, std::string> categoryNameMap;
static std::map<std::string, std::string> categoryDescriptionMap;
-Addons::Addons(SDL_Renderer &renderer, ImageManager &imageManager):selected(NULL){
+Addons::Addons(SDL_Renderer &renderer, ImageManager &imageManager)
+ : selected(NULL), categoryList(NULL), categoryDescription(NULL), list(NULL)
+{
//Render the title.
title = titleTextureFromText(renderer, _("Addons"), objThemes.getTextColor(false), SCREEN_WIDTH);
//Load placeholder addon icons and screenshot.
addonIcon["levels"] = imageManager.loadImage(getDataPath() + "/gfx/addon1.png");
addonIcon["levelpacks"] = imageManager.loadImage(getDataPath() + "/gfx/addon2.png");
addonIcon["themes"] = imageManager.loadImage(getDataPath() + "/gfx/addon3.png");
addonIcon[std::string()] = imageManager.loadImage(getDataPath() + "/gfx/addon0.png");
//Load predefined categories.
if (categoryNameMap.empty()) {
for (int i = 0; predefinedCategories[i]; i += 3) {
categoryNameMap[predefinedCategories[i]] = predefinedCategories[i + 1];
categoryDescriptionMap[predefinedCategories[i]] = predefinedCategories[i + 2];
}
}
screenshot=imageManager.loadTexture(getDataPath()+"/gfx/screenshot.png", renderer);
-
- //Open the addons file in the user cache path for writing (downloading) to.
- FILE* addon=fopen((getUserPath(USER_CACHE)+"addons").c_str(),"wb");
//Clear the GUI if any.
if(GUIObjectRoot){
delete GUIObjectRoot;
GUIObjectRoot=NULL;
}
//Show a loading screen
{
int w = 0, h = 0;
SDL_GetRendererOutputSize(&renderer, &w, &h);
SDL_Color fg = { 255, 255, 255, 0 };
TexturePtr loadingTexture = titleTextureFromText(renderer, _("Loading..."), fg, w);
SDL_Rect loadingRect = rectFromTexture(*loadingTexture);
loadingRect.x = (w - loadingRect.w) / 2;
loadingRect.y = (h - loadingRect.h) / 2;
SDL_SetRenderDrawColor(&renderer, 0, 0, 0, 255);
SDL_RenderClear(&renderer);
SDL_RenderCopy(&renderer, loadingTexture.get(), NULL, &loadingRect);
SDL_RenderPresent(&renderer);
SDL_RenderClear(&renderer);
}
//Try to get(download) the addonsList.
- if(getAddonsList(addon, renderer, imageManager)==false){
+ bool ret = getAddonsList(renderer, imageManager);
+
+ if(!ret) {
+ if (error.empty()) error = " ";
+
//It failed so we show the error message.
GUIObjectRoot=new GUIObject(imageManager,renderer,0,0,SCREEN_WIDTH,SCREEN_HEIGHT);
GUIObject* obj=new GUILabel(imageManager,renderer,90,96,200,32,_("Unable to initialize addon menu:"));
obj->name="lbl";
GUIObjectRoot->addChild(obj);
obj=new GUILabel(imageManager,renderer,120,130,200,32,error.c_str());
obj->name="lbl";
GUIObjectRoot->addChild(obj);
obj=new GUIButton(imageManager,renderer,90,550,200,32,_("Back"));
obj->name="cmdBack";
obj->eventCallback=this;
GUIObjectRoot->addChild(obj);
return;
}
//Now create the GUI.
createGUI(renderer, imageManager);
}
Addons::~Addons(){
//If the GUIObjectRoot exist delete it.
if(GUIObjectRoot){
delete GUIObjectRoot;
GUIObjectRoot=NULL;
}
}
void Addons::createGUI(SDL_Renderer& renderer, ImageManager& imageManager){
//Downloaded the addons file now we can create the GUI.
GUIObjectRoot=new GUIObject(imageManager,renderer,0,0,SCREEN_WIDTH,SCREEN_HEIGHT);
//Create list of categories
categoryList = new GUISingleLineListBox(imageManager, renderer, (SCREEN_WIDTH - 500) / 2, 100, 500, 32);
categoryList->name="lstTabs";
//Loop through the categories and add them to the list.
//FIXME: Hack for easy detecting which categories there are.
{
set<string> categories;
for (const auto& a : addons) {
categories.insert(a.type);
}
for (const auto& c : categories) {
auto it = categoryNameMap.find(c);
categoryList->addItem(c, it == categoryNameMap.end() ? c.c_str() : _(it->second));
}
}
categoryList->value=0;
categoryList->eventCallback=this;
GUIObjectRoot->addChild(categoryList);
//category description
categoryDescription = new GUILabel(imageManager, renderer, 0, 136, SCREEN_WIDTH, 32, "", 0, true, true, GUIGravityCenter);
if (categoryList->value >= 0 && categoryList->value < (int)categoryList->item.size()) {
auto it = categoryDescriptionMap.find(categoryList->item[categoryList->value].first);
if (it != categoryDescriptionMap.end()) categoryDescription->caption = _(it->second);
}
GUIObjectRoot->addChild(categoryDescription);
//Create the list for the addons.
//By default levels will be selected.
list=new GUIListBox(imageManager,renderer,SCREEN_WIDTH*0.1,176,SCREEN_WIDTH*0.8,SCREEN_HEIGHT-228);
addonsToList(categoryList->getName(), renderer, imageManager);
list->name="lstAddons";
list->clickEvents=true;
list->eventCallback=this;
list->value=-1;
GUIObjectRoot->addChild(list);
type="levels";
//The back button.
GUIObject* obj=new GUIButton(imageManager,renderer,20,20,-1,32,_("Back"));
obj->name="cmdBack";
obj->eventCallback=this;
GUIObjectRoot->addChild(obj);
}
-bool Addons::getAddonsList(FILE* file, SDL_Renderer& renderer, ImageManager& imageManager){
+bool Addons::getAddonsList(SDL_Renderer& renderer, ImageManager& imageManager){
//First we download the file.
- if(downloadFile(getSettings()->getValue("addon_url"),file)==false){
+ FILE* file = fopen((getUserPath(USER_CACHE) + "addons").c_str(), "wb");
+ bool downloadStatus = downloadFile(getSettings()->getValue("addon_url"), file);
+ fclose(file);
+
+ if (!downloadStatus){
//NOTE: We keep the console output English so we put the string literal here twice.
cerr<<"ERROR: unable to download addons file!"<<endl;
error=_("ERROR: unable to download addons file!");
return false;
}
- fclose(file);
-
+
//Load the downloaded file.
ifstream addonFile;
addonFile.open((getUserPath(USER_CACHE)+"addons").c_str());
if(!addonFile.good()) {
//NOTE: We keep the console output English so we put the string literal here twice.
cerr<<"ERROR: unable to load addon_list file!"<<endl;
/// TRANSLATORS: addon_list is the name of a file and should not be translated.
error=_("ERROR: unable to load addon_list file!");
return false;
}
//Parse the addonsfile.
TreeStorageNode obj;
{
POASerializer objSerializer;
if(!objSerializer.readNode(addonFile,&obj,true)){
//NOTE: We keep the console output English so we put the string literal here twice.
cerr<<"ERROR: Invalid file format of addons file!"<<endl;
error=_("ERROR: Invalid file format of addons file!");
return false;
}
}
//Check the addon version in the addons list.
int version=0;
if(!obj.attributes["version"].empty())
version=atoi(obj.attributes["version"][0].c_str());
if(version<MIN_VERSION || version>MAX_VERSION){
//NOTE: We keep the console output English so we put the string literal here twice.
cerr<<"ERROR: Addon list version is unsupported! (received: "<<version<<" supported:"<<MIN_VERSION<<"-"<<MAX_VERSION<<")"<<endl;
error=_("ERROR: Addon list version is unsupported!");
return false;
}
//Also load the installed_addons file.
ifstream iaddonFile;
iaddonFile.open((getUserPath(USER_CONFIG)+"installed_addons").c_str());
if(!iaddonFile) {
//The installed_addons file doesn't exist, so we create it.
ofstream iaddons;
iaddons.open((getUserPath(USER_CONFIG)+"installed_addons").c_str());
iaddons<<" "<<endl;
iaddons.close();
//Also load the installed_addons file.
iaddonFile.open((getUserPath(USER_CONFIG)+"installed_addons").c_str());
if(!iaddonFile) {
//NOTE: We keep the console output English so we put the string literal here twice.
cerr<<"ERROR: Unable to create the installed_addons file."<<endl;
/// TRANSLATORS: installed_addons is the name of a file and should not be translated.
error=_("ERROR: Unable to create the installed_addons file.");
return false;
}
}
//And parse the installed_addons file.
TreeStorageNode obj1;
- {
- POASerializer objSerializer;
- if(!objSerializer.readNode(iaddonFile,&obj1,true)){
- //NOTE: We keep the console output English so we put the string literal here twice.
- cerr<<"ERROR: Invalid file format of the installed_addons!"<<endl;
- error=_("ERROR: Invalid file format of the installed_addons!");
- return false;
- }
+ if (!POASerializer().readNode(iaddonFile, &obj1, true)){
+ iaddonFile.close();
+
+ //In this case we should erase installed_addons so that the user is possible to enter addon screen next time.
+ ofstream iaddons;
+ iaddons.open((getUserPath(USER_CONFIG) + "installed_addons").c_str());
+ iaddons << " " << endl;
+ iaddons.close();
+
+ //NOTE: We keep the console output English so we put the string literal here twice.
+ cerr<<"ERROR: Invalid file format of the installed_addons!"<<endl;
+ error=_("ERROR: Invalid file format of the installed_addons!");
+ return false;
}
-
-
+
+
//Fill the vector.
fillAddonList(obj,obj1, renderer, imageManager);
//Close the files.
iaddonFile.close();
addonFile.close();
return true;
}
void Addons::fillAddonList(TreeStorageNode &objAddons, TreeStorageNode &objInstalledAddons,
SDL_Renderer& renderer, ImageManager& imageManager){
//Loop through the blocks of the addons file.
//These should contain the types levels, levelpacks, themes.
for(unsigned int i=0;i<objAddons.subNodes.size();i++){
TreeStorageNode* block=objAddons.subNodes[i];
if(block==NULL) continue;
//Check what kind of block it is, only category at the moment.
if(block->name=="category" && block->value.size()>0){
string type=block->value[0];
//Now loop the entries(subNodes) of the block.
for(unsigned int i=0;i<block->subNodes.size();i++){
TreeStorageNode* entry=block->subNodes[i];
if(entry==NULL) continue;
if(entry->name=="entry" && entry->value.size()==1){
//The entry is valid so create a new Addon.
Addon addon;
addon.icon=nullptr;
addon.screenshot=nullptr;
addon.type=type;
addon.name=entry->value[0];
addon.version = 0;
addon.installedVersion = 0;
if(!entry->attributes["file"].empty())
addon.file=entry->attributes["file"][0];
if(!entry->attributes["author"].empty())
addon.author=entry->attributes["author"][0];
if(!entry->attributes["description"].empty())
addon.description=entry->attributes["description"][0];
if(!entry->attributes["license"].empty())
addon.license=entry->attributes["license"][0];
if(!entry->attributes["website"].empty())
addon.website=entry->attributes["website"][0];
if(entry->attributes["icon"].size()>1){
//There are (at least) two values, the url to the icon and its md5sum used for caching.
addon.icon=loadCachedImage(
entry->attributes["icon"][0].c_str(),
entry->attributes["icon"][1].c_str(),
imageManager
);
}
if(entry->attributes["screenshot"].size()>1){
//There are (at least) two values, the url to the screenshot and its md5sum used for caching.
addon.screenshot=loadCachedTexture(
entry->attributes["screenshot"][0].c_str(),
entry->attributes["screenshot"][1].c_str(),
renderer,
imageManager
);
}
if(!entry->attributes["version"].empty())
addon.version=atoi(entry->attributes["version"][0].c_str());
addon.upToDate=false;
addon.installed=false;
//Check if the addon is already installed.
for(unsigned int i=0;i<objInstalledAddons.subNodes.size();i++){
TreeStorageNode* installed=objInstalledAddons.subNodes[i];
if(installed==NULL) continue;
if(installed->name=="entry" && installed->value.size()==3){
if(addon.type.compare(installed->value[0])==0 && addon.name.compare(installed->value[1])==0) {
addon.installed=true;
addon.installedVersion=atoi(installed->value[2].c_str());
if(addon.installedVersion>=addon.version) {
addon.upToDate=true;
}
//Read the dependencies and content from the file.
for(unsigned int j=0;j<installed->subNodes.size();j++){
if(installed->subNodes[j]->name=="content"){
TreeStorageNode* obj=installed->subNodes[j];
for(unsigned int k=0;k<obj->subNodes.size();k++){
if(obj->subNodes[k]->value.size()==1)
addon.content.push_back(pair<string,string>(obj->subNodes[k]->name,obj->subNodes[k]->value[0]));
}
}else if(installed->subNodes[j]->name=="dependencies"){
TreeStorageNode* obj=installed->subNodes[j];
for(unsigned int k=0;k<obj->subNodes.size();k++){
if(obj->subNodes[k]->value.size()==1)
addon.dependencies.push_back(pair<string,string>(obj->subNodes[k]->name,obj->subNodes[k]->value[0]));
}
}
}
}
}
}
//Finally put him in the list.
addons.push_back(addon);
}
}
}
}
}
void Addons::addonsToList(const std::string &type, SDL_Renderer& renderer, ImageManager&){
//Clear the list.
list->clearItems();
//Loop through the addons.
for(unsigned int i=0;i<addons.size();i++) {
//Make sure the addon is of the requested type.
if(addons[i].type!=type)
continue;
const Addon& addon=addons[i];
string entry=addon.name+" by "+addon.author;
if(addon.installed){
if(addon.upToDate){
entry+=" *";
}else{
entry+=" +";
}
}
SurfacePtr surf = createSurface(list->width,74);
//Check if there's an icon for the addon.
if(addon.icon){
applySurface(5, 5, addon.icon, surf.get(), NULL);
}else{
auto it = addonIcon.find(type);
if (it == addonIcon.end()) it = addonIcon.find(std::string());
assert(it != addonIcon.end());
applySurface(5, 5, it->second, surf.get(), NULL);
}
SDL_Surface* nameSurf=TTF_RenderUTF8_Blended(fontGUI,addon.name.c_str(),objThemes.getTextColor(true));
SDL_SetSurfaceAlphaMod(nameSurf,0xFF);
applySurface(74,-1,nameSurf,surf.get(),NULL);
SDL_FreeSurface(nameSurf);
/// TRANSLATORS: indicates the author of an addon.
string authorLine = tfm::format(_("by %s"),addon.author);
SDL_Surface* authorSurf=TTF_RenderUTF8_Blended(fontText,authorLine.c_str(),objThemes.getTextColor(true));
SDL_SetSurfaceAlphaMod(authorSurf,0xFF);
applySurface(74,43,authorSurf,surf.get(),NULL);
SDL_FreeSurface(authorSurf);
if(addon.installed){
if(addon.upToDate){
SDL_Surface* infoSurf=TTF_RenderUTF8_Blended(fontText,_("Installed"),objThemes.getTextColor(true));
SDL_SetSurfaceAlphaMod(infoSurf,0xFF);
applySurface(surf->w-infoSurf->w-32,(surf->h-infoSurf->h)/2,infoSurf,surf.get(),NULL);
SDL_FreeSurface(infoSurf);
}else{
SDL_Surface* infoSurf=TTF_RenderUTF8_Blended(fontText,_("Updatable"),objThemes.getTextColor(true));
SDL_SetSurfaceAlphaMod(infoSurf,0xFF);
applySurface(surf->w-infoSurf->w-32,(surf->h-infoSurf->h)/2,infoSurf,surf.get(),NULL);
SDL_FreeSurface(infoSurf);
}
}else{
SDL_Color c = objThemes.getTextColor(true);
c.r = c.r / 2 + 128;
c.g = c.g / 2 + 128;
c.b = c.b / 2 + 128;
SDL_Surface* infoSurf = TTF_RenderUTF8_Blended(fontText, _("Not installed"), c);
SDL_SetSurfaceAlphaMod(infoSurf,0xFF);
applySurface(surf->w-infoSurf->w-32,(surf->h-infoSurf->h)/2,infoSurf,surf.get(),NULL);
SDL_FreeSurface(infoSurf);
}
list->addItem(renderer,entry,textureFromSurface(renderer,std::move(surf)));
}
}
bool Addons::saveInstalledAddons(){
+ //If there is error loading addons file we doesn't save installed_addons at all.
+ if (!error.empty()) return false;
+
//Open the file.
ofstream iaddons;
iaddons.open((getUserPath(USER_CONFIG)+"installed_addons").c_str());
if(!iaddons) return false;
TreeStorageNode installed;
//Loop through all the addons.
vector<Addon>::iterator it;
for(it=addons.begin();it!=addons.end();++it){
//Check if the level is installed or not.
if(it->installed) {
TreeStorageNode *entry=new TreeStorageNode;
entry->name="entry";
entry->value.push_back(it->type);
entry->value.push_back(it->name);
char version[64];
sprintf(version,"%d",it->installedVersion);
entry->value.push_back(version);
//Now add a subNode for each content.
TreeStorageNode* content=new TreeStorageNode;
content->name="content";
for(unsigned int i=0;i<it->content.size();i++){
TreeStorageNode* contentEntry=new TreeStorageNode;
contentEntry->name=it->content[i].first;
contentEntry->value.push_back(it->content[i].second);
//Add the content node to the entry node.
content->subNodes.push_back(contentEntry);
}
entry->subNodes.push_back(content);
//Now add a sub node for the dependencies.
TreeStorageNode* deps=new TreeStorageNode;
deps->name="dependencies";
for(unsigned int i=0;i<it->dependencies.size();i++){
TreeStorageNode* depsEntry=new TreeStorageNode;
depsEntry->name=it->dependencies[i].first;
depsEntry->value.push_back(it->dependencies[i].second);
//Add the content node to the entry node.
deps->subNodes.push_back(depsEntry);
}
entry->subNodes.push_back(deps);
//And add the entry to the top node.
installed.subNodes.push_back(entry);
}
}
//And write away the file.
POASerializer objSerializer;
objSerializer.writeNode(&installed,iaddons,true,true);
return true;
}
SharedTexture Addons::loadCachedTexture(const char* url,const char* md5sum,
SDL_Renderer& renderer, ImageManager& imageManager){
//Check if the image is cached.
string imageFile=getUserPath(USER_CACHE)+"images/"+md5sum;
if(fileExists(imageFile.c_str())){
//It is, so load the image.
return imageManager.loadTexture(imageFile, renderer);
}else{
//Download the image.
FILE* file=fopen(imageFile.c_str(),"wb");
//Downloading failed.
if(!downloadFile(url,file)){
cerr<<"ERROR: Unable to download image from "<<url<<endl;
fclose(file);
return NULL;
}
fclose(file);
//Load the image.
return imageManager.loadTexture(imageFile, renderer);
}
}
SDL_Surface* Addons::loadCachedImage(const char* url, const char* md5sum,
ImageManager& imageManager){
//Check if the image is cached.
string imageFile = getUserPath(USER_CACHE) + "images/" + md5sum;
if (fileExists(imageFile.c_str())){
//It is, so load the image.
return imageManager.loadImage(imageFile);
} else{
//Download the image.
FILE* file = fopen(imageFile.c_str(), "wb");
//Downloading failed.
if (!downloadFile(url, file)){
cerr << "ERROR: Unable to download image from " << url << endl;
fclose(file);
return NULL;
}
fclose(file);
//Load the image.
return imageManager.loadImage(imageFile);
}
}
void Addons::handleEvents(ImageManager& imageManager, SDL_Renderer& renderer){
//Check if we should quit.
if(event.type==SDL_QUIT){
//Save the installed addons before exiting.
saveInstalledAddons();
setNextState(STATE_EXIT);
}
//Check if escape is pressed, if so return to the main menu.
if(inputMgr.isKeyDownEvent(INPUTMGR_ESCAPE)){
setNextState(STATE_MENU);
}
+ if (categoryList == NULL) return;
+
//Check horizontal movement
int value = categoryList->value;
if (inputMgr.isKeyDownEvent(INPUTMGR_RIGHT)){
isKeyboardOnly = true;
value++;
if (value >= (int)categoryList->item.size()) value = 0;
} else if (inputMgr.isKeyDownEvent(INPUTMGR_LEFT)){
isKeyboardOnly = true;
value--;
if (value < 0) value = categoryList->item.size() - 1;
}
if (value >= 0 && value < (int)categoryList->item.size()) {
if (categoryList->value != value) {
categoryList->value = value;
GUIEventCallback_OnEvent(imageManager, renderer, categoryList->name, categoryList, GUIEventChange);
return;
}
//Check vertical movement
if (inputMgr.isKeyDownEvent(INPUTMGR_UP)){
isKeyboardOnly = true;
list->value--;
if (list->value < 0) list->value = 0;
//FIXME: ad-hoc stupid code
list->scrollScrollbar(0xC0000000);
list->scrollScrollbar(list->value);
} else if (inputMgr.isKeyDownEvent(INPUTMGR_DOWN)){
isKeyboardOnly = true;
list->value++;
if (list->value >= (int)list->item.size()) list->value = list->item.size() - 1;
//FIXME: ad-hoc stupid code
list->scrollScrollbar(0xC0000000);
list->scrollScrollbar(list->value);
}
if (isKeyboardOnly && inputMgr.isKeyDownEvent(INPUTMGR_SELECT) && list->value >= 0 && list->value<(int)list->item.size()) {
GUIEventCallback_OnEvent(imageManager, renderer, list->name, list, GUIEventChange); // ???
GUIEventCallback_OnEvent(imageManager, renderer, list->name, list, GUIEventClick);
return;
}
}
}
void Addons::logic(ImageManager&, SDL_Renderer&){}
void Addons::render(ImageManager&, SDL_Renderer& renderer){
//Draw background.
objThemes.getBackground(true)->draw(renderer);
//Draw the title.
drawTitleTexture(SCREEN_WIDTH, *title, renderer);
}
void Addons::resize(ImageManager& imageManager, SDL_Renderer& renderer){
//Delete the gui (if any).
if(GUIObjectRoot){
delete GUIObjectRoot;
GUIObjectRoot=NULL;
}
//Now create a new one.
createGUI(renderer, imageManager);
}
void Addons::showAddon(ImageManager& imageManager, SDL_Renderer& renderer){
//Make sure an addon is selected.
if(!selected)
return;
//Skip next mouse up event since we're clicking a list box and showing a new window.
GUISkipNextMouseUpEvent = true;
//Create a root object.
- GUIObject* root=new GUIFrame(imageManager,renderer,(SCREEN_WIDTH-600)/2,(SCREEN_HEIGHT-400)/2,600,400,selected->name.c_str());
+ GUIObject* root=new GUIFrame(imageManager,renderer,(SCREEN_WIDTH-760)/2,(SCREEN_HEIGHT-560)/2,760,560,selected->name.c_str());
//Create the 'by creator' label.
- GUIObject* obj=new GUILabel(imageManager,renderer,0,50,600,50,tfm::format(_("by %s"),selected->author).c_str(),0,true,true,GUIGravityCenter);
+ GUIObject* obj=new GUILabel(imageManager,renderer,0,45,760,50,tfm::format(_("by %s"),selected->author).c_str(),0,true,true,GUIGravityCenter);
root->addChild(obj);
//Create the description text.
std::string s = tfm::format(_("Version: %d\n"), selected->version);
if (selected->installed) {
s += tfm::format(_("Installed version: %d\n"), selected->installedVersion);
}
if (!selected->license.empty()) {
s += tfm::format(_("License: %s\n"), appendURLToLicense(selected->license));
}
if (!selected->website.empty()) {
s += tfm::format(_("Website: %s\n"), selected->website);
}
s += '\n';
if (selected->description.empty()) {
s += _("(No descriptions provided)");
} else {
s += selected->description;
}
- GUITextArea* description=new GUITextArea(imageManager,renderer,10,100,370,200);
+ GUITextArea* description=new GUITextArea(imageManager,renderer,10,90,530,390);
description->setString(renderer, s, true);
description->editable=false;
description->onResize();
description->extractHyperlinks();
root->addChild(description);
//Create the screenshot image. (If a screenshot is missing, we use the default screenshot.)
- GUIImage* img=new GUIImage(imageManager,renderer,390,100,200,150,selected->screenshot?selected->screenshot:screenshot);
+ GUIImage* img=new GUIImage(imageManager,renderer,550,90,200,150,selected->screenshot?selected->screenshot:screenshot);
root->addChild(img);
GUIButton *cancelButton;
//Add buttons depending on the installed/update status.
if(selected->installed && !selected->upToDate){
- GUIObject* bRemove=new GUIButton(imageManager,renderer,root->width*0.97,350,-1,32,_("Remove"),0,true,true,GUIGravityRight);
+ GUIObject* bRemove=new GUIButton(imageManager,renderer,root->width*0.95,510,-1,32,_("Remove"),0,true,true,GUIGravityRight);
bRemove->name="cmdRemove";
bRemove->eventCallback=this;
root->addChild(bRemove);
//Create a back button.
- cancelButton = new GUIButton(imageManager, renderer, root->width*0.03, 350, -1, 32, _("Back"), 0, true, true, GUIGravityLeft);
+ cancelButton = new GUIButton(imageManager, renderer, root->width*0.05, 510, -1, 32, _("Back"), 0, true, true, GUIGravityLeft);
cancelButton->name = "cmdCloseOverlay";
cancelButton->eventCallback = this;
root->addChild(cancelButton);
//Update widget sizes.
root->render(renderer, 0,0,false);
//Create a nicely centered button.
obj = new GUIButton(imageManager, renderer,
- (int)floor((cancelButton->left + cancelButton->width + bRemove->left - bRemove->width)*0.5), 350,
+ (int)floor((cancelButton->left + cancelButton->width + bRemove->left - bRemove->width)*0.5), 510,
-1, 32, _("Update"), 0, true, true, GUIGravityCenter);
obj->name="cmdUpdate";
obj->eventCallback=this;
root->addChild(obj);
}else{
if(!selected->installed){
- obj=new GUIButton(imageManager,renderer,root->width*0.9,350,-1,32,_("Install"),0,true,true,GUIGravityRight);
+ obj=new GUIButton(imageManager,renderer,root->width*0.9,510,-1,32,_("Install"),0,true,true,GUIGravityRight);
obj->name="cmdInstall";
obj->eventCallback=this;
root->addChild(obj);
}else if(selected->upToDate){
- obj=new GUIButton(imageManager,renderer,root->width*0.9,350,-1,32,_("Remove"),0,true,true,GUIGravityRight);
+ obj=new GUIButton(imageManager,renderer,root->width*0.9,510,-1,32,_("Remove"),0,true,true,GUIGravityRight);
obj->name="cmdRemove";
obj->eventCallback=this;
root->addChild(obj);
}
//Create a back button.
- cancelButton = new GUIButton(imageManager, renderer, root->width*0.1, 350, -1, 32, _("Back"), 0, true, true, GUIGravityLeft);
+ cancelButton = new GUIButton(imageManager, renderer, root->width*0.1, 510, -1, 32, _("Back"), 0, true, true, GUIGravityLeft);
cancelButton->name = "cmdCloseOverlay";
cancelButton->eventCallback = this;
root->addChild(cancelButton);
}
new AddonOverlay(renderer, root, cancelButton, description, TabFocus | ReturnControls);
}
void Addons::GUIEventCallback_OnEvent(ImageManager& imageManager, SDL_Renderer& renderer, std::string name,GUIObject* obj,int eventType){
if(name=="lstTabs"){
//Get the category type.
type=categoryList->getName();
//Get the description of current category.
auto it = categoryDescriptionMap.find(type);
if (it != categoryDescriptionMap.end()) categoryDescription->caption = _(it->second);
else categoryDescription->caption.clear();
//Get the list corresponding with the category and select the first entry.
addonsToList(type, renderer, imageManager);
list->value=-1;
//Call an event as if an entry in the addons listbox was clicked.
GUIEventCallback_OnEvent(imageManager, renderer, "lstAddons",list,GUIEventChange);
}else if(name=="lstAddons"){
//Check which type of event.
if(eventType==GUIEventChange){
//Get the addon struct that belongs to it.
Addon* addon=NULL;
//Make sure the addon list on screen isn't empty.
if (list->value >= 0 && list->value < (int)list->item.size()){
//Get the name of the (newly) selected entry.
string entry=list->getItem(list->value);
//Loop through the addons of the selected category.
for(unsigned int i=0;i<addons.size();i++){
//Make sure the addons are of the same type.
if(addons[i].type!=categoryList->getName())
continue;
string prefix=addons[i].name;
if(!entry.compare(0,prefix.size(),prefix)){
addon=&addons[i];
}
}
}
//Set the new addon as selected and unselect the list.
selected=addon;
if (!isKeyboardOnly) list->value = -1;
}else if(eventType==GUIEventClick){
//Make sure an addon is selected.
if(selected){
showAddon(imageManager,renderer);
}
}
}else if(name=="cmdBack"){
saveInstalledAddons();
setNextState(STATE_MENU);
}else if(name=="cmdCloseOverlay"){
//We can safely delete the GUIObjectRoot, since it's handled by the GUIOverlay.
delete GUIObjectRoot;
GUIObjectRoot=NULL;
}else if(name=="cmdUpdate"){
//NOTE: This simply removes the addon and reinstalls it.
//The complete addon is downloaded either way so no need for checking what has been changed/added/removed/etc...
if(selected){
removeAddon(imageManager,renderer,selected);
installAddon(imageManager,renderer,selected);
}
addonsToList(categoryList->getName(), renderer, imageManager);
}else if(name=="cmdInstall"){
if(selected)
installAddon(imageManager,renderer,selected);
addonsToList(categoryList->getName(), renderer, imageManager);
}else if(name=="cmdRemove"){
//TODO: Check for dependencies.
//Loop through the addons to check if this addon is a dependency of another addon.
vector<Addon>::iterator it;
for(it=addons.begin();it!=addons.end();++it){
//Check if the addon has dependencies.
if(!it->dependencies.empty()){
vector<pair<string,string> >::iterator depIt;
for(depIt=it->dependencies.begin();depIt!=it->dependencies.end();++depIt){
if(depIt->first=="addon" && depIt->second==selected->name){
msgBox(imageManager,renderer,tfm::format(_("This addon can't be removed because it's needed by %s."),it->name),MsgBoxOKOnly,_("Dependency"));
return;
}
}
}
}
if(selected)
removeAddon(imageManager,renderer,selected);
addonsToList(categoryList->getName(), renderer, imageManager);
}
//NOTE: In case of install/remove/update we can delete the GUIObjectRoot, since it's managed by the GUIOverlay.
if(name=="cmdUpdate" || name=="cmdInstall" || name=="cmdRemove"){
delete GUIObjectRoot;
GUIObjectRoot=NULL;
}
}
void Addons::removeAddon(ImageManager& imageManager,SDL_Renderer& renderer, Addon* addon){
//To remove an addon we loop over the content vector in the structure.
//NOTE: This should contain all INSTALLED content, if something failed during installation it isn't added.
for(unsigned int i=0;i<addon->content.size();i++){
//Check the type of content.
if(addon->content[i].first=="file"){
string file=getUserPath(USER_DATA)+addon->content[i].second;
//Check if the file exists.
if(!fileExists(file.c_str())){
cerr<<"WARNING: File '"<<file<<"' appears to have been removed already."<<endl;
msgBox(imageManager,renderer,tfm::format(_("WARNING: File '%s' appears to have been removed already."),file),MsgBoxOKOnly,_("Addon error"));
continue;
}
//Remove the file.
if(!removeFile(file.c_str())){
cerr<<"ERROR: Unable to remove file '"<<file<<"'!"<<endl;
msgBox(imageManager,renderer,tfm::format(_("ERROR: Unable to remove file '%s'!"),file),MsgBoxOKOnly,_("Addon error"));
continue;
}
}else if(addon->content[i].first=="folder"){
string dir=getUserPath(USER_DATA)+addon->content[i].second;
//Check if the directory exists.
if(!dirExists(dir.c_str())){
cerr<<"WARNING: Directory '"<<dir<<"' appears to have been removed already."<<endl;
msgBox(imageManager,renderer,tfm::format(_("WARNING: Directory '%s' appears to have been removed already."),dir),MsgBoxOKOnly,_("Addon error"));
continue;
}
//Remove the directory.
if(!removeDirectory(dir.c_str())){
cerr<<"ERROR: Unable to remove directory '"<<dir<<"'!"<<endl;
msgBox(imageManager,renderer,tfm::format(_("ERROR: Unable to remove directory '%s'!"),dir),MsgBoxOKOnly,_("Addon error"));
continue;
}
}else if(addon->content[i].first=="level"){
string file=getUserPath(USER_DATA)+"levels/"+addon->content[i].second;
//Check if the level file exists.
if(!fileExists(file.c_str())){
cerr<<"WARNING: Level '"<<file<<"' appears to have been removed already."<<endl;
msgBox(imageManager,renderer,tfm::format(_("WARNING: Level '%s' appears to have been removed already."),file),MsgBoxOKOnly,_("Addon error"));
continue;
}
//Remove the level file.
if(!removeFile(file.c_str())){
cerr<<"ERROR: Unable to remove level '"<<file<<"'!"<<endl;
msgBox(imageManager,renderer,tfm::format(_("ERROR: Unable to remove level '%s'!"),file),MsgBoxOKOnly,_("Addon error"));
continue;
}
//Also remove the level from the Levels levelpack.
LevelPack* levelsPack=getLevelPackManager()->getLevelPack(LEVELS_PATH);
for(int i=0;i<levelsPack->getLevelCount();i++){
if(levelsPack->getLevelFile(i)==file){
//Remove the level and break out of the loop.
levelsPack->removeLevel(i);
break;
}
}
}else if(addon->content[i].first=="levelpack"){
//FIXME: We assume no trailing slash since there mustn't be one for installing, bad :(
string dir=getUserPath(USER_DATA)+"levelpacks/"+addon->content[i].second+"/";
//Check if the directory exists.
if(!dirExists(dir.c_str())){
cerr<<"WARNING: Levelpack directory '"<<dir<<"' appears to have been removed already."<<endl;
msgBox(imageManager,renderer,tfm::format(_("WARNING: Levelpack directory '%s' appears to have been removed already."),dir),MsgBoxOKOnly,_("Addon error"));
continue;
}
//Remove the directory.
if(!removeDirectory(dir.c_str())){
cerr<<"ERROR: Unable to remove levelpack directory '"<<dir<<"'!"<<endl;
msgBox(imageManager,renderer,tfm::format(_("ERROR: Unable to remove levelpack directory '%s'!"),dir),MsgBoxOKOnly,_("Addon error"));
continue;
}
//Also remove the levelpack from the levelpackManager.
getLevelPackManager()->removeLevelPack(dir, true);
}
}
//Now that the content has been removed clear the content list itself.
addon->content.clear();
//And finally set the addon to not installed.
addon->installed=false;
addon->installedVersion=0;
//Also clear the 'offline' information.
addon->content.clear();
addon->dependencies.clear();
}
void Addons::installAddon(ImageManager& imageManager,SDL_Renderer& renderer, Addon* addon){
string tmpDir=getUserPath(USER_CACHE)+"tmp/";
string fileName=fileNameFromPath(addon->file,true);
//Download the selected addon to the tmp folder.
if(!downloadFile(addon->file,tmpDir)){
cerr<<"ERROR: Unable to download addon file "<<addon->file<<endl;
msgBox(imageManager,renderer,tfm::format(_("ERROR: Unable to download addon file %s."),addon->file),MsgBoxOKOnly,_("Addon error"));
return;
}
//Now extract the addon.
if(!extractFile(tmpDir+fileName,tmpDir+"/addon/")){
cerr<<"ERROR: Unable to extract addon file "<<addon->file<<endl;
msgBox(imageManager,renderer,tfm::format(_("ERROR: Unable to extract addon file %s."),addon->file),MsgBoxOKOnly,_("Addon error"));
return;
}
ifstream metadata((tmpDir+"/addon/metadata").c_str());
if(!metadata){
cerr<<"ERROR: Addon is missing metadata!"<<endl;
msgBox(imageManager,renderer,_("ERROR: Addon is missing metadata!"),MsgBoxOKOnly,_("Addon error"));
return;
}
//Read the metadata from the addon.
TreeStorageNode obj;
{
POASerializer objSerializer;
if(!objSerializer.readNode(metadata,&obj,true)){
//NOTE: We keep the console output English so we put the string literal here twice.
cerr<<"ERROR: Invalid file format for metadata file!"<<endl;
msgBox(imageManager,renderer,_("ERROR: Invalid file format for metadata file!"),MsgBoxOKOnly,_("Addon error"));
return;
}
}
//Loop through the subNodes.
for(unsigned int i=0;i<obj.subNodes.size();i++){
//Check for the content subNode (there should only be one).
if(obj.subNodes[i]->name=="content"){
TreeStorageNode* obj1=obj.subNodes[i];
//Loop through the subNodes of that.
for(unsigned int j=0;j<obj1->subNodes.size();j++){
TreeStorageNode* obj2=obj1->subNodes[j];
//This code happens for all types of content.
string source=tmpDir+"addon/content/";
if(obj2->value.size()>0)
source+=obj2->value[0];
//The destination MUST be in the user data path.
string dest=getUserPath(USER_DATA);
if(obj2->value.size()>1)
dest+=obj2->value[1];
//Check what the content type is.
if(obj2->name=="file" && obj2->value.size()==2){
//Now copy the file.
if(fileExists(dest.c_str())){
cerr<<"WARNING: File '"<<dest<<"' already exists, addon may be broken or not working!"<<endl;
msgBox(imageManager,renderer,tfm::format(_("WARNING: File '%s' already exists, addon may be broken or not working!"),dest),MsgBoxOKOnly,_("Addon error"));
continue;
}
if(!copyFile(source.c_str(),dest.c_str())){
cerr<<"WARNING: Unable to copy file '"<<source<<"' to '"<<dest<<"', addon may be broken or not working!"<<endl;
msgBox(imageManager,renderer,tfm::format(_("WARNING: Unable to copy file '%s' to '%s', addon may be broken or not working!"),source,dest),MsgBoxOKOnly,_("Addon error"));
continue;
}
//Add it to the content vector.
addon->content.push_back(pair<string,string>("file",obj2->value[1]));
}else if(obj2->name=="folder" && obj2->value.size()==2){
//The dest must NOT exist, otherwise it will fail.
if(dirExists(dest.c_str())){
cerr<<"WARNING: Destination directory '"<<dest<<"' already exists, addon may be broken or not working!"<<endl;
msgBox(imageManager,renderer,tfm::format(_("WARNING: Destination directory '%s' already exists, addon may be broken or not working!"),dest),MsgBoxOKOnly,_("Addon error"));
continue;
}
//FIXME: Copy the directory instead of renaming it, in case the same folder/parts of the folder are needed in different places.
if(!renameDirectory(source.c_str(),dest.c_str())){
cerr<<"WARNING: Unable to move directory '"<<source<<"' to '"<<dest<<"', addon may be broken or not working!"<<endl;
msgBox(imageManager,renderer,tfm::format(_("WARNING: Unable to move directory '%s' to '%s', addon may be broken or not working!"),source,dest),MsgBoxOKOnly,_("Addon error"));
continue;
}
//Add it to the content vector.
addon->content.push_back(pair<string,string>("folder",obj2->value[1]));
}else if(obj2->name=="level" && obj2->value.size()==1){
//The destination MUST be in the levels folder in the user data path.
dest+="levels/"+fileNameFromPath(source);
//Now copy the file.
if(fileExists(dest.c_str())){
cerr<<"WARNING: Level '"<<dest<<"' already exists, addon may be broken or not working!"<<endl;
msgBox(imageManager,renderer,tfm::format(_("WARNING: Level '%s' already exists, addon may be broken or not working!"),dest),MsgBoxOKOnly,_("Addon error"));
continue;
}
if(!copyFile(source.c_str(),dest.c_str())){
cerr<<"WARNING: Unable to copy level '"<<source<<"' to '"<<dest<<"', addon may be broken or not working!"<<endl;
msgBox(imageManager,renderer,tfm::format(_("WARNING: Unable to copy level '%s' to '%s', addon may be broken or not working!"),source,dest),MsgBoxOKOnly,_("Addon error"));
continue;
}
//It's a level so add it to the Levels levelpack.
LevelPack* levelsPack=getLevelPackManager()->getLevelPack(LEVELS_PATH);
if(levelsPack){
levelsPack->addLevel(dest);
levelsPack->setLocked(levelsPack->getLevelCount()-1);
}else{
cerr<<"ERROR: Unable to add level to Levels levelpack"<<endl;
}
addon->content.push_back(pair<string,string>("level",fileNameFromPath(source)));
}else if(obj2->name=="levelpack" && obj2->value.size()==1){
//TODO: Check if the source contains a trailing slash.
//The destination MUST be in the user data path.
dest+="levelpacks/"+fileNameFromPath(source);
//The dest must NOT exist, otherwise it will fail.
if(dirExists(dest.c_str())){
cerr<<"WARNING: Levelpack directory '"<<dest<<"' already exists, addon may be broken or not working!"<<endl;
msgBox(imageManager,renderer,tfm::format(_("WARNING: Levelpack directory '%s' already exists, addon may be broken or not working!"),dest),MsgBoxOKOnly,_("Addon error"));
continue;
}
//FIXME: Copy the directory instead of renaming it, in case the same folder/parts of the folder are needed in different places.
if(!renameDirectory(source.c_str(),dest.c_str())){
cerr<<"WARNING: Unable to move directory '"<<source<<"' to '"<<dest<<"', addon may be broken or not working!"<<endl;
msgBox(imageManager,renderer,tfm::format(_("WARNING: Unable to move directory '%s' to '%s', addon may be broken or not working!"),source,dest),MsgBoxOKOnly,_("Addon error"));
continue;
}
//It's a levelpack so add it to the levelpack manager.
getLevelPackManager()->loadLevelPack(dest);
addon->content.push_back(pair<string,string>("levelpack",fileNameFromPath(source)));
}
}
}else if(obj.subNodes[i]->name=="dependencies"){
TreeStorageNode* obj1=obj.subNodes[i];
//Loop through the subNodes of that.
for(unsigned int j=0;j<obj1->subNodes.size();j++){
TreeStorageNode* obj2=obj1->subNodes[j];
if(obj2->name=="addon" && obj2->value.size()>0){
Addon* dep=NULL;
//Check if the requested addon can be found.
vector<Addon>::iterator it;
for(it=addons.begin();it!=addons.end();++it){
if(it->name==obj2->value[0]){
dep=&(*it);
break;
}
}
if(!dep){
cerr<<"ERROR: Addon requires another addon ("<<obj2->value[0]<<") which can't be found!"<<endl;
msgBox(imageManager, renderer, tfm::format(_("ERROR: Addon requires another addon (%s) which can't be found!"), obj2->value[0]), MsgBoxOKOnly, _("Addon error"));
continue;
}
//The addon has been found, try to install it.
//FIXME: Somehow prevent recursion, maybe max depth (??)
if(!dep->installed){
msgBox(imageManager, renderer, tfm::format(_("The addon %s is needed and will be installed now."), dep->name), MsgBoxOKOnly, _("Dependency"));
installAddon(imageManager,renderer, dep);
}
//Add the dependency to the addon.
addon->dependencies.push_back(pair<string,string>("addon",dep->name));
}
}
}
}
//The addon is installed and up to date, but not necessarily flawless.
addon->installed=true;
addon->upToDate=true;
addon->installedVersion=addon->version;
}
diff --git a/src/Addons.h b/src/Addons.h
index d9940ce..2e7808b 100644
--- a/src/Addons.h
+++ b/src/Addons.h
@@ -1,165 +1,161 @@
/*
* Copyright (C) 2011-2013 Me and My Shadow
*
* This file is part of Me and My Shadow.
*
* Me and My Shadow is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Me and My Shadow is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Me and My Shadow. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef ADDONS_H
#define ADDONS_H
#include "GameState.h"
#include "GameObjects.h"
#include "GUIObject.h"
#include "GUIListBox.h"
#include <array>
#include <vector>
#include <string>
//The addons menu.
class Addons: public GameState,public GUIEventCallback{
private:
//The minimum addon version that is supported.
static const int MIN_VERSION=2;
//The maximum addon version that is supported.
static const int MAX_VERSION=2;
//An addon entry.
struct Addon{
//The name of the addon.
string name;
//The type of addon. (Level, Levelpack, Theme)
string type;
//The link to the addon file.
string file;
//The name of the author.
string author;
//The description of the addon.
string description;
//The license of the addon.
string license;
//The website of the addon.
string website;
//Icon for the addon.
SDL_Surface* icon;
//Screenshot for the addon.
SharedTexture screenshot;
//The latest version of the addon.
int version;
//The version that the user has installed, if installed.
int installedVersion;
//Boolean if the addon is installed.
bool installed;
//Boolean if the addon is upToDate. (installedVersion==version)
bool upToDate;
//Map that contains the content of the addon.
//NOTE: This is only filled if the addon is installed.
std::vector<std::pair<std::string,std::string> > content;
//Array that holds the name of the addons it's dependent on.
//NOTE: This is only filled if the addon is installed.
std::vector<std::pair<std::string,std::string> > dependencies;
};
//The title.
TexturePtr title;
//Placeholder icons for addons in case they don't provide custom icons.
std::map<std::string, SDL_Surface*> addonIcon;
//Placeholder screenshot for addons in case they don't provide one.
SharedTexture screenshot;
//Map containing a vector of Addons for each addon category.
std::vector<Addon> addons;
- //File pointing to the addon file in the userpath.
- FILE* addon;
-
//String that should contain the error when something fails.
string error;
//The type of addon that is currently selected.
string type;
//Pointer to the addon that is selected.
Addon* selected;
//The list used for the selecting of the category.
GUISingleLineListBox* categoryList;
//Pointer to the description.
GUIObject* categoryDescription;
//The list used for listing the addons.
GUIListBox* list;
public:
//Constructor.
Addons(SDL_Renderer& renderer, ImageManager& imageManager);
//Destructor.
~Addons();
//Method that will create the GUI.
void createGUI(SDL_Renderer &renderer, ImageManager &imageManager);
//Method that loads that downloads the addons list.
- //file: Pointer to the file to download the list to.
//Returns: True if the file is downloaded successfuly.
- bool getAddonsList(FILE* file, SDL_Renderer& renderer, ImageManager& imageManager);
- //
- void fillAddonList(TreeStorageNode &objAddons,TreeStorageNode &objInstalledAddons,SDL_Renderer& renderer, ImageManager& imageManager);
+ bool getAddonsList(SDL_Renderer& renderer, ImageManager& imageManager);
+
+ void fillAddonList(TreeStorageNode &objAddons,TreeStorageNode &objInstalledAddons,SDL_Renderer& renderer, ImageManager& imageManager);
//Put all the addons of a given type in a vector.
//type: The type the addons must be.
//Returns: Vector containing the addons.
void addonsToList(const string &type, SDL_Renderer &renderer, ImageManager &);
//Method that will save the installed addons to the installed_addons file.
//Returns: True if the file is saved successfuly.
bool saveInstalledAddons();
//Method for loading a cached image and downloading if it isn't cached.
//url: The url to the image.
//md5sum: The md5sum used for caching.
//Returns: Shared pointer to the loaded image.
SDL_Surface* loadCachedImage(const char* url,const char* md5sum, ImageManager& imageManager);
SharedTexture loadCachedTexture(const char* url, const char* md5sum, SDL_Renderer& renderer, ImageManager& imageManager);
//Method that will open a GUIOverlay with the an overview of the selected addon.
void showAddon(ImageManager& imageManager,SDL_Renderer& renderer);
//Inherited from GameState.
void handleEvents(ImageManager&, SDL_Renderer&) override;
void logic(ImageManager&, SDL_Renderer&) override;
void render(ImageManager&, SDL_Renderer& renderer) override;
void resize(ImageManager &imageManager, SDL_Renderer& renderer) override;
//Method used for GUI event handling.
//name: The name of the callback.
//obj: Pointer to the GUIObject that caused the event.
//eventType: The type of event: click, change, etc..
void GUIEventCallback_OnEvent(ImageManager& imageManager, SDL_Renderer& renderer, std::string name,GUIObject* obj,int eventType);
//This method will remove the addon based on the content vector.
//NOTE It doesn't check if the addon is installed or not.
//addon: The addon to remove.
void removeAddon(ImageManager& imageManager,SDL_Renderer &renderer, Addon* addon);
//This method will install the addon by downloading,extracting and reading.
//NOTE It doesn't check if the addon is installed or not.
//addon: The addon to install.
void installAddon(ImageManager& imageManager, SDL_Renderer &renderer, Addon* addon);
};
#endif
diff --git a/src/Commands.cpp b/src/Commands.cpp
index f64b957..58b1191 100644
--- a/src/Commands.cpp
+++ b/src/Commands.cpp
@@ -1,1067 +1,1063 @@
/*
* Copyright (C) 2018 Me and My Shadow
*
* This file is part of Me and My Shadow.
*
* Me and My Shadow is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Me And My Shadow is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Me and My Shadow. If not, see <http://www.gnu.org/licenses/>.
*/
// An undo/redo system based on <http://www.codeproject.com/Articles/2500/A-Basic-Undo-Redo-Framework-For-C>.
// Originally written by squarecross <https://forum.freegamedev.net/viewtopic.php?f=48&t=5432>.
#include "Commands.h"
#include "GameObjects.h"
#include "Block.h"
#include "LevelEditor.h"
#include "Globals.h"
#include "Functions.h"
#include <algorithm>
#include <string>
#include <vector>
#include <map>
#include <stdio.h>
#include "libs/tinyformat/tinyformat.h"
Command::~Command() {
}
//////////////////////////////MoveGameObjectCommand///////////////////////////////////
MoveGameObjectCommand::MoveGameObjectCommand(LevelEditor* levelEditor, GameObject* gameObject, int x, int y, int w, int h)
: editor(levelEditor), objects(1, gameObject)
, resizeCommand(NULL)
{
SDL_Rect r = gameObject->getBox();
r.x = x;
r.y = y;
if (w >= 0) r.w = w;
if (h >= 0) r.h = h;
newPosition.push_back(r);
init();
}
MoveGameObjectCommand::MoveGameObjectCommand(LevelEditor* levelEditor, std::vector<GameObject*>& gameObjects, int dx, int dy)
: editor(levelEditor), objects(gameObjects)
, resizeCommand(NULL)
{
for (auto obj : objects) {
SDL_Rect r = obj->getBox();
r.x += dx;
r.y += dy;
newPosition.push_back(r);
}
init();
}
void MoveGameObjectCommand::init() {
//Initialize old position.
for (auto obj : objects) {
SDL_Rect r = obj->getBox();
oldPosition.push_back(r);
}
// Create resize command if necessary.
resizeCommand = ResizeLevelCommand::createAndShiftIfNecessary(editor, newPosition);
}
MoveGameObjectCommand::~MoveGameObjectCommand(){
if (resizeCommand) delete resizeCommand;
}
void MoveGameObjectCommand::execute(){
// First resize the level if necessary.
if (resizeCommand) resizeCommand->execute();
// Set the obj at its new position.
for (int i = 0; i < (int)objects.size(); i++) {
const SDL_Rect &r = newPosition[i];
GameObject *obj = objects[i];
obj->setBaseLocation(r.x, r.y);
obj->setBaseSize(r.w, r.h);
}
}
void MoveGameObjectCommand::unexecute() {
// First undo the resize if necessary.
if (resizeCommand) resizeCommand->unexecute();
// Set the obj at its old position.
for (int i = 0; i < (int)objects.size(); i++) {
const SDL_Rect &r = oldPosition[i];
GameObject *obj = objects[i];
obj->setBaseLocation(r.x, r.y);
obj->setBaseSize(r.w, r.h);
}
}
ResizeLevelCommand::ResizeLevelCommand(LevelEditor* levelEditor, int newWidth, int newHeight, int diffx, int diffy)
: editor(levelEditor), newLevelWidth(newWidth), newLevelHeight(newHeight), diffx(diffx), diffy(diffy)
{
oldLevelWidth = LEVEL_WIDTH;
oldLevelHeight = LEVEL_HEIGHT;
}
ResizeLevelCommand::~ResizeLevelCommand() {
}
void ResizeLevelCommand::execute() {
resizeLevel(editor, newLevelWidth, newLevelHeight, diffx, diffy);
}
void ResizeLevelCommand::unexecute() {
resizeLevel(editor, oldLevelWidth, oldLevelHeight, -diffx, -diffy);
}
void ResizeLevelCommand::resizeLevel(LevelEditor* levelEditor, int newWidth, int newHeight, int diffx, int diffy) {
LEVEL_WIDTH = newWidth;
LEVEL_HEIGHT = newHeight;
if (diffx != 0 || diffy != 0) {
camera.x += diffx;
camera.y += diffy;
for (unsigned int o = 0; o < levelEditor->levelObjects.size(); o++){
SDL_Rect r = levelEditor->levelObjects[o]->getBox();
levelEditor->levelObjects[o]->setBaseLocation(r.x + diffx, r.y + diffy);
}
for (auto it = levelEditor->sceneryLayers.begin(); it != levelEditor->sceneryLayers.end(); ++it) {
for (auto o : it->second->objects){
SDL_Rect r = o->getBox();
o->setBaseLocation(r.x + diffx, r.y + diffy);
}
}
}
}
ResizeLevelCommand* ResizeLevelCommand::createAndShiftIfNecessary(LevelEditor* levelEditor, std::vector<SDL_Rect>& position) {
// Calculate new level size, shift, etc.
int newLevelWidth = LEVEL_WIDTH;
int newLevelHeight = LEVEL_HEIGHT;
int diffx = 0, diffy = 0;
for (int i = 0; i < (int)position.size(); i++) {
const SDL_Rect &r = position[i];
if (r.x + r.w > newLevelWidth) {
newLevelWidth = r.x + r.w;
}
if (r.y + r.h > newLevelHeight) {
newLevelHeight = r.y + r.h;
}
if (r.x + diffx < 0) diffx = -r.x;
if (r.y + diffy < 0) diffy = -r.y;
}
newLevelWidth += diffx;
newLevelHeight += diffy;
if (newLevelWidth != LEVEL_WIDTH || newLevelHeight != LEVEL_HEIGHT || diffx || diffy) {
if (diffx || diffy) {
for (int i = 0; i < (int)position.size(); i++) {
SDL_Rect &r = position[i];
r.x += diffx;
r.y += diffy;
}
}
return new ResizeLevelCommand(levelEditor, newLevelWidth, newLevelHeight, diffx, diffy);
} else {
return NULL;
}
}
std::string ResizeLevelCommand::describe() {
/// TRANSLATORS: Context: Undo/Redo ...
return _("Resize level");
}
//////////////////////////////AddRemoveGameObjectCommand///////////////////////////////////
AddRemoveGameObjectCommand::AddRemoveGameObjectCommand(LevelEditor* levelEditor, GameObject* gameObject, bool isAdd_)
: editor(levelEditor), objects(1, gameObject), isAdd(isAdd_), ownObject(isAdd_)
, resizeCommand(NULL), removeStartCommand(NULL), oldTriggers(NULL)
{
init();
}
AddRemoveGameObjectCommand::AddRemoveGameObjectCommand(LevelEditor* levelEditor, std::vector<GameObject*>& gameObjects, bool isAdd_)
: editor(levelEditor), objects(gameObjects), isAdd(isAdd_), ownObject(isAdd_)
, resizeCommand(NULL), removeStartCommand(NULL), oldTriggers(NULL)
{
init();
}
void AddRemoveGameObjectCommand::init() {
if (isAdd) {
theLayer = editor->selectedLayer;
// adjust the object position if necessary
std::vector<SDL_Rect> position;
for (auto obj : objects) {
position.push_back(obj->getBox());
}
resizeCommand = ResizeLevelCommand::createAndShiftIfNecessary(editor, position);
if (resizeCommand) {
for (int i = 0; i < (int)objects.size(); i++) {
const SDL_Rect &r = position[i];
objects[i]->setBaseLocation(r.x, r.y);
}
}
// remove the existing player/shadow start if necessary
std::vector<bool> typesToBeRemoved((int)TYPE_MAX, false);
bool removeSomething = false;
for (auto obj : objects) {
if (obj->type == TYPE_START_PLAYER || obj->type == TYPE_START_SHADOW) {
typesToBeRemoved[obj->type] = true;
removeSomething = true;
}
}
if (removeSomething) {
std::vector<GameObject*> objectsToBeRemoved;
for (auto obj : editor->levelObjects) {
if (typesToBeRemoved[obj->type]) objectsToBeRemoved.push_back(obj);
}
if (!objectsToBeRemoved.empty()) {
removeStartCommand = new AddRemoveGameObjectCommand(editor, objectsToBeRemoved, false);
}
}
}
}
void AddRemoveGameObjectCommand::backupTriggers() {
if (oldTriggers == NULL) oldTriggers = new LevelEditor::Triggers(editor->triggers);
}
AddRemoveGameObjectCommand::~AddRemoveGameObjectCommand(){
//Remove the objects if we own them.
if (ownObject) {
for (auto obj : objects) {
delete obj;
}
}
//Delete internal commands.
if (resizeCommand) delete resizeCommand;
if (removeStartCommand) delete removeStartCommand;
//Delete old triggers
if (oldTriggers) delete oldTriggers;
}
void AddRemoveGameObjectCommand::addGameObject(){
// some sanity check
assert(ownObject);
// Remove old start position if necessary.
if (removeStartCommand) removeStartCommand->execute();
// Resize the level if necessary.
if (resizeCommand) resizeCommand->execute();
// Add each object to level
for (int index = 0; index < (int)objects.size(); index++) {
GameObject *obj = objects[index];
//Increase totalCollectables everytime we add a new collectable
if (obj->type == TYPE_COLLECTABLE) {
editor->totalCollectables++;
}
Block* block = dynamic_cast<Block*>(obj);
Scenery* scenery = dynamic_cast<Scenery*>(obj);
//Add it to the levelObjects.
if (theLayer.empty()) {
assert(block != NULL);
editor->levelObjects.push_back(block);
} else {
assert(scenery != NULL);
auto it = editor->sceneryLayers.find(theLayer);
assert(it != editor->sceneryLayers.end());
it->second->objects.push_back(scenery);
}
//GameObject type specific stuff.
switch (obj->type){
case TYPE_BUTTON:
case TYPE_SWITCH:
case TYPE_PORTAL:
{
//If block doesn't have an id (new object).
if (block->getEditorProperty("id").empty()) {
//Give it it's own id.
char s[64];
sprintf(s, "%u", editor->currentId);
editor->currentId++;
block->setEditorProperty("id", s);
}
break;
}
case TYPE_MOVING_BLOCK:
case TYPE_MOVING_SHADOW_BLOCK:
case TYPE_MOVING_SPIKES:
{
//Add the object to the moving blocks.
vector<MovingPosition> positions;
editor->movingBlocks[block] = positions;
//Get the moving position.
const vector<SDL_Rect> &movingPos = block->movingPos;
//Add the object to the movingBlocks vector.
editor->movingBlocks[block].clear();
for (int i = 0, m = movingPos.size(); i < m; i++) {
MovingPosition position(movingPos[i].x, movingPos[i].y, movingPos[i].w);
editor->movingBlocks[block].push_back(position);
}
//If block doesn't have an id.
if (block->getEditorProperty("id").empty()) {
//Give it it's own id.
char s[64];
sprintf(s, "%u", editor->currentId);
editor->currentId++;
block->setEditorProperty("id", s);
}
break;
}
default:
break;
}
}
// now we doesn't own the object anymore.
ownObject = false;
}
void AddRemoveGameObjectCommand::removeGameObject(){
// some sanity check
assert(!ownObject);
//Make sure we don't access the removed object through
//moving/linking.
editor->movingBlock = nullptr;
editor->linkingTrigger = nullptr;
editor->moving = false;
editor->linking = false;
// Remove objects
for (int index = 0; index < (int)objects.size(); index++) {
GameObject *obj = objects[index];
std::vector<GameObject*>::iterator it;
std::map<Block*, vector<GameObject*> >::iterator mapIt;
//Decrease totalCollectables everytime we remove a collectable
if (obj->type == TYPE_COLLECTABLE){
editor->totalCollectables--;
}
//Check if the object is in the selection.
it = find(editor->selection.begin(), editor->selection.end(), obj);
if (it != editor->selection.end()){
//It is so we delete it.
editor->selection.erase(it);
}
Block *theBlock = dynamic_cast<Block*>(obj);
Scenery *theScenery = dynamic_cast<Scenery*>(obj);
//Check if the object is in the triggers.
if (theBlock) {
mapIt = editor->triggers.find(theBlock);
if (mapIt != editor->triggers.end()){
//Back up triggers if it's remove mode.
if (!isAdd) backupTriggers();
//Remove the object from triggers.
editor->triggers.erase(mapIt);
}
}
//Boolean if it could be a target.
if (obj->type == TYPE_MOVING_BLOCK || obj->type == TYPE_MOVING_SHADOW_BLOCK || obj->type == TYPE_MOVING_SPIKES
|| obj->type == TYPE_CONVEYOR_BELT || obj->type == TYPE_SHADOW_CONVEYOR_BELT || obj->type == TYPE_PORTAL)
{
for (mapIt = editor->triggers.begin(); mapIt != editor->triggers.end(); ++mapIt){
//Now loop the target vector.
for (int o = mapIt->second.size() - 1; o >= 0; o--){
//Check if the obj is in the target vector.
if (mapIt->second[o] == obj){
//Back up triggers if it's remove mode.
if (!isAdd) backupTriggers();
//Remove the object from triggers.
mapIt->second.erase(mapIt->second.begin() + o);
}
}
}
}
//Check if the object is in the movingObjects.
if (theBlock) {
std::map<Block*, vector<MovingPosition> >::iterator movIt;
movIt = editor->movingBlocks.find(theBlock);
if (movIt != editor->movingBlocks.end()){
//It is so we remove it.
editor->movingBlocks.erase(movIt);
}
}
//Check if the block isn't being configured with a window one way or another.
for (;;) {
std::map<GUIObject*, GameObject*>::iterator confIt;
for (confIt = editor->objectWindows.begin(); confIt != editor->objectWindows.end(); ++confIt){
if (confIt->second == obj) break;
}
if (confIt == editor->objectWindows.end()) break;
editor->destroyWindow(confIt->first);
}
//Now we remove the object from the levelObjects and/or scenery.
if (theBlock){
std::vector<Block*>::iterator it;
it = find(editor->levelObjects.begin(), editor->levelObjects.end(), theBlock);
if (it != editor->levelObjects.end()){
editor->levelObjects.erase(it);
theLayer.clear(); // record the layer of this object
}
} else if (theScenery) {
for (auto it = editor->sceneryLayers.begin(); it != editor->sceneryLayers.end(); ++it){
auto it2 = find(it->second->objects.begin(), it->second->objects.end(), theScenery);
if (it2 != it->second->objects.end()) {
it->second->objects.erase(it2);
theLayer = it->first; // record the layer of this object
break;
}
}
}
}
// Resize the level if necessary.
if (resizeCommand) resizeCommand->unexecute();
// Restore old start position if necessary.
if (removeStartCommand) removeStartCommand->unexecute();
// now we own this object
ownObject = true;
//Set dirty of selection popup
editor->selectionDirty();
}
void AddRemoveGameObjectCommand::execute(){
if (isAdd) {
addGameObject();
} else {
removeGameObject();
}
}
void AddRemoveGameObjectCommand::unexecute(){
if (isAdd) {
removeGameObject();
} else {
addGameObject();
}
// Restore old triggers if necessary
if (oldTriggers) editor->triggers = *oldTriggers;
}
//////////////////////////////AddRemovePathCommand///////////////////////////////////
AddRemovePathCommand::AddRemovePathCommand(LevelEditor* levelEditor, Block* movingBlock, MovingPosition movingPosition, bool isAdd_)
:editor(levelEditor), target(movingBlock), movePos(movingPosition), isAdd(isAdd_)
{
if (!isAdd) {
if (target->movingPos.empty()) {
assert(!"movingBlock->movingPos is empty!");
} else {
const SDL_Rect& r = target->movingPos.back();
movePos.x = r.x;
movePos.y = r.y;
movePos.time = r.w;
}
}
}
AddRemovePathCommand::~AddRemovePathCommand(){
}
void AddRemovePathCommand::execute(){
if (isAdd) addPath();
else removePath();
}
void AddRemovePathCommand::unexecute(){
if (isAdd) removePath();
else addPath();
}
void AddRemovePathCommand::addPath() {
//Add movePos to the path.
editor->movingBlocks[target].push_back(movePos);
//Write the path to the moving block.
setEditorData();
}
void AddRemovePathCommand::removePath() {
//Remove the last point in the path.
editor->movingBlocks[target].pop_back();
//Write the path to the moving block.
setEditorData();
}
void AddRemovePathCommand::setEditorData() {
target->movingPos.clear();
for (const MovingPosition& p : editor->movingBlocks[target]) {
SDL_Rect r = { p.x, p.y, p.time };
target->movingPos.push_back(r);
}
}
//////////////////////////////RemovePathCommand///////////////////////////////////
RemovePathCommand::RemovePathCommand(LevelEditor* levelEditor, Block* targetBlock)
:editor(levelEditor), target(targetBlock){
}
RemovePathCommand::~RemovePathCommand(){
}
void RemovePathCommand::execute(){
//Set the number of movingPositions to 0.
target->setEditorProperty("MovingPosCount","0");
std::map<Block*,vector<MovingPosition> >::iterator it;
//Check if target is in movingBlocks.
it = editor->movingBlocks.find(target);
if(it!=editor->movingBlocks.end()){
//Store the movingPositions for unexecute.
movePositions = it->second;
//Clear all its movingPositions
it->second.clear();
}
}
void RemovePathCommand::unexecute(){
std::map<Block*,vector<MovingPosition> >::iterator it;
//Find target in movingBlocks
it = editor->movingBlocks.find(target);
if(it!=editor->movingBlocks.end()){
//Restore its movingPositions
it->second = movePositions;
//Write the path to the moving block.
std::map<std::string,std::string> editorData;
char s[64], s0[64];
sprintf(s,"%d",int(editor->movingBlocks[target].size()));
editorData["MovingPosCount"]=s;
//Loop through the positions.
for(unsigned int o=0; o<editor->movingBlocks[target].size(); o++) {
sprintf(s0+1,"%u",o);
sprintf(s,"%d",editor->movingBlocks[target][o].x);
s0[0]='x';
editorData[s0]=s;
sprintf(s,"%d",editor->movingBlocks[target][o].y);
s0[0]='y';
editorData[s0]=s;
sprintf(s,"%d",editor->movingBlocks[target][o].time);
s0[0]='t';
editorData[s0]=s;
}
target->setEditorData(editorData);
}
}
//////////////////////////////AddLinkCommand///////////////////////////////////
AddLinkCommand::AddLinkCommand(LevelEditor* levelEditor, Block* linkingTrigger, GameObject* clickedObject)
:editor(levelEditor), target(linkingTrigger), clickedObj(clickedObject),oldPortalLink(NULL), destination(""), id(""), oldTrigger(NULL){
}
AddLinkCommand::~AddLinkCommand(){
}
void AddLinkCommand::execute(){
//Check if the target can handle multiple or only one link.
switch(target->type) {
case TYPE_PORTAL: {
//Store the last portal link.
if(!editor->triggers[target].empty()){
oldPortalLink = editor->triggers[target].back();
}
//Portals can only link to one so remove all existing links.
editor->triggers[target].clear();
editor->triggers[target].push_back(clickedObj);
break;
}
default: {
//The others can handle multiple links.
editor->triggers[target].push_back(clickedObj);
break;
}
}
//Check if it's a portal.
if(target->type==TYPE_PORTAL) {
//Store the previous destination.
destination = target->getEditorProperty("destination");
//Portals need to get the id of the other instead of give it's own id.
- char s[64];
- sprintf(s,"%d",atoi(clickedObj->getEditorProperty("id").c_str()));
- target->setEditorProperty("destination",s);
+ target->setEditorProperty("destination", clickedObj->getEditorProperty("id"));
} else{
//Store the previous id.
id = clickedObj->getEditorProperty("id");
if(!id.empty()) {
//Another block might already be set as trigger for the clickedObj.
for(auto block : editor->levelObjects) {
std::string otherId = block->getEditorProperty("id");
if(id.compare(otherId) == 0) {
//IDs match, so either one of the trigger itself or one of its (other) targets.
std::vector<GameObject*>::iterator it;
//Find the clickedObj in the block's triggers.
it = std::find(editor->triggers[block].begin(), editor->triggers[block].end(), clickedObj);
if(it != editor->triggers[block].end()){
oldTrigger = block;
editor->triggers[block].erase(it);
break;
}
}
}
}
//Give the clickedObject the same id as the trigger.
- char s[64];
- sprintf(s,"%d",atoi(target->getEditorProperty("id").c_str()));
- clickedObj->setEditorProperty("id",s);
+ clickedObj->setEditorProperty("id", target->getEditorProperty("id"));
}
}
void AddLinkCommand::unexecute(){
//Check if the target can handle multiple or only one link.
switch(target->type) {
case TYPE_PORTAL: {
//Portals can only link to one so remove all existing links.
editor->triggers[target].clear();
//Put the previous portal link back.
if(oldPortalLink != NULL)
editor->triggers[target].push_back(oldPortalLink);
break;
}
default:{
std::vector<GameObject*>::iterator it;
//Find the clickedObj in the target's triggers.
it = std::find(editor->triggers[target].begin(), editor->triggers[target].end(), clickedObj);
if(it != editor->triggers[target].end()){
//Remove it.
editor->triggers[target].erase(it);
}
//Restore old trigger if applicable
if(oldTrigger != NULL){
editor->triggers[oldTrigger].push_back(clickedObj);
}
break;
}
}
//Check if it's a portal.
if(target->type==TYPE_PORTAL) {
//Restore previous destination.
target->setEditorProperty("destination",destination);
} else{
//Restore clickedObj's previous id.
clickedObj->setEditorProperty("id",id);
}
}
//////////////////////////////RemoveLinkCommand///////////////////////////////////
RemoveLinkCommand::RemoveLinkCommand(LevelEditor* levelEditor, Block* targetBlock)
: editor(levelEditor), target(targetBlock), destination(""), id(""){
}
RemoveLinkCommand::~RemoveLinkCommand(){
}
void RemoveLinkCommand::execute(){
std::map<Block*,vector<GameObject*> >::iterator it;
//Find target in triggers.
it = editor->triggers.find(target);
if(it!=editor->triggers.end()) {
//Copy the objects the target was linked to.
links = it->second;
//Remove the links.
it->second.clear();
}
//In case of a portal remove its destination field.
if(target->type==TYPE_PORTAL) {
//Copy its previous destination.
destination = target->getEditorProperty("destination");
//Erase its destination.
target->setEditorProperty("destination","");
} else{
//Copy its previous id.
id = target->getEditorProperty("id");
//Give the trigger a new id to prevent activating unlinked targets.
char s[64];
sprintf(s,"%u",editor->currentId);
editor->currentId++;
target->setEditorProperty("id",s);
}
}
void RemoveLinkCommand::unexecute(){
std::map<Block*,vector<GameObject*> >::iterator it;
//Find target in triggers.
it = editor->triggers.find(target);
if(it!=editor->triggers.end()) {
//Restore objects it was linked to.
it->second = links;
}
if(target->type== TYPE_PORTAL){
//Restore old destination.
target->setEditorProperty("destination", destination);
} else{
//Restore old id.
target->setEditorProperty("id", id);
}
}
SetEditorPropertyCommand::SetEditorPropertyCommand(LevelEditor* levelEditor, ImageManager& imageManager, SDL_Renderer& renderer, GameObject* targetBlock,
const std::string& propertyName, const std::string& propertyValue, const std::string& propertyDescription)
: editor(levelEditor), imageManager(imageManager), renderer(renderer)
, target(targetBlock), prop(propertyName), newValue(propertyValue), desc(propertyDescription)
{
oldValue = target->getEditorProperty(prop);
}
SetEditorPropertyCommand::~SetEditorPropertyCommand() {
}
void SetEditorPropertyCommand::execute() {
target->setEditorProperty(prop, newValue);
updateCustomScenery();
}
void SetEditorPropertyCommand::unexecute() {
target->setEditorProperty(prop, oldValue);
updateCustomScenery();
}
void SetEditorPropertyCommand::updateCustomScenery() {
Scenery *scenery = dynamic_cast<Scenery*>(target);
if (target && prop == "customScenery") {
scenery->updateCustomScenery(imageManager, renderer);
}
}
SetLevelPropertyCommand::~SetLevelPropertyCommand() {
}
void SetLevelPropertyCommand::execute() {
setLevelProperty(newProperty);
}
void SetLevelPropertyCommand::unexecute() {
setLevelProperty(oldProperty);
}
std::string SetLevelPropertyCommand::describe() {
/// TRANSLATORS: Context: Undo/Redo ...
return _("Modify level property");
}
SetScriptCommand::SetScriptCommand(LevelEditor* levelEditor, Block* targetBlock, const std::map<int, std::string>& script, const std::string& id)
: editor(levelEditor), target(targetBlock), newScript(script), id(id)
{
if (target) {
oldScript = target->scripts;
oldId = target->id;
} else {
oldScript = editor->scripts;
}
}
SetScriptCommand::~SetScriptCommand() {
}
void SetScriptCommand::execute() {
setScript(newScript, id);
}
void SetScriptCommand::unexecute() {
setScript(oldScript, oldId);
}
void SetScriptCommand::setScript(const std::map<int, std::string>& script, const std::string& id) {
if (target) {
target->scripts = script;
//Set the new id for the target block.
//TODO: Check for trigger links etc...
target->id = id;
} else {
editor->scripts = script;
}
}
AddRemoveLayerCommand::AddRemoveLayerCommand(LevelEditor* levelEditor, const std::string& layerName, bool isAdd_)
: editor(levelEditor), layer(NULL), theLayer(layerName), isAdd(isAdd_), ownObject(isAdd_)
{
if (isAdd) {
layer = new SceneryLayer;
} else {
auto it = editor->sceneryLayers.find(theLayer);
if (it != editor->sceneryLayers.end()) {
layer = it->second;
}
}
}
void AddRemoveLayerCommand::execute() {
if (isAdd) {
addLayer();
} else {
removeLayer();
}
}
void AddRemoveLayerCommand::unexecute() {
if (isAdd) {
removeLayer();
} else {
addLayer();
}
}
void AddRemoveLayerCommand::addLayer() {
assert(ownObject && layer);
assert(editor->sceneryLayers.find(theLayer) == editor->sceneryLayers.end());
// add this layer
editor->sceneryLayers[theLayer] = layer;
// show and select the newly created layer
editor->layerVisibility[theLayer] = true;
editor->selectedLayer = theLayer;
// deselect all
editor->deselectAll();
// change the ownership
ownObject = false;
}
void AddRemoveLayerCommand::removeLayer() {
assert(!ownObject);
assert(editor->sceneryLayers.find(theLayer) != editor->sceneryLayers.end());
assert(editor->sceneryLayers.find(theLayer)->second == layer);
// deselect all
editor->deselectAll();
// select the Blocks layer
editor->selectedLayer.clear();
// remove this layer
editor->layerVisibility.erase(theLayer);
editor->sceneryLayers.erase(theLayer);
// change the ownership
ownObject = true;
}
AddRemoveLayerCommand::~AddRemoveLayerCommand() {
if (ownObject) {
delete layer;
}
}
std::string AddRemoveLayerCommand::describe() {
return tfm::format(isAdd ?
/// TRANSLATORS: Context: Undo/Redo ...
_("Add scenery layer %s") :
/// TRANSLATORS: Context: Undo/Redo ...
_("Delete scenery layer %s"), theLayer);
}
SetLayerPropertyCommand::SetLayerPropertyCommand(LevelEditor* levelEditor, const std::string& oldName, const LayerProperty& newProperty)
: editor(levelEditor), newProperty(newProperty)
{
auto it = editor->sceneryLayers.find(oldName);
assert(it != editor->sceneryLayers.end() && it->second);
oldProperty.name = oldName;
oldProperty.speedX = it->second->speedX;
oldProperty.speedY = it->second->speedY;
oldProperty.cameraX = it->second->cameraX;
oldProperty.cameraY = it->second->cameraY;
}
void SetLayerPropertyCommand::execute() {
setLayerProperty(oldProperty.name, newProperty);
}
void SetLayerPropertyCommand::unexecute() {
setLayerProperty(newProperty.name, oldProperty);
}
SetLayerPropertyCommand::~SetLayerPropertyCommand() {
}
std::string SetLayerPropertyCommand::describe() {
/// TRANSLATORS: Context: Undo/Redo ...
return tfm::format(_("Modify the property of scenery layer %s"), oldProperty.name);
}
void SetLayerPropertyCommand::setLayerProperty(const std::string& oldName, const LayerProperty& newProperty) {
SceneryLayer *tmp = NULL;
// Find the existing layer
{
auto it = editor->sceneryLayers.find(oldName);
assert(it != editor->sceneryLayers.end() && it->second);
tmp = it->second;
}
// Check if we need to rename scenery layer
if (oldName != newProperty.name) {
assert(editor->sceneryLayers.find(newProperty.name) == editor->sceneryLayers.end());
// remove the old layer
editor->sceneryLayers.erase(oldName);
// then save the temp variable to the new layer
editor->sceneryLayers[newProperty.name] = tmp;
// show and select the newly created layer
editor->layerVisibility[newProperty.name] = editor->layerVisibility[oldName];
editor->layerVisibility.erase(oldName);
editor->selectedLayer = newProperty.name;
}
// Now we update the properties of the layer
tmp->speedX = newProperty.speedX;
tmp->speedY = newProperty.speedY;
tmp->cameraX = newProperty.cameraX;
tmp->cameraY = newProperty.cameraY;
}
MoveToLayerCommand::MoveToLayerCommand(LevelEditor* levelEditor, std::vector<GameObject*>& gameObjects, const std::string& oldName, const std::string& newName)
: editor(levelEditor), oldName(oldName), newName(newName), createNewLayer(NULL)
{
if (editor->sceneryLayers.find(newName) == editor->sceneryLayers.end()) {
createNewLayer = new AddRemoveLayerCommand(levelEditor, newName, true);
}
for (auto obj : gameObjects) {
Scenery *scenery = dynamic_cast<Scenery*>(obj);
if (scenery) objects.push_back(scenery);
}
}
void MoveToLayerCommand::execute() {
if (createNewLayer) {
createNewLayer->execute();
}
removeGameObject();
addGameObject(newName);
// show and select the new layer
editor->layerVisibility[newName] = true;
editor->selectedLayer = newName;
// deselect all
editor->deselectAll();
}
void MoveToLayerCommand::unexecute() {
removeGameObject();
addGameObject(oldName);
if (createNewLayer) {
createNewLayer->unexecute();
}
// show and select the old layer
editor->layerVisibility[oldName] = true;
editor->selectedLayer = oldName;
// deselect all
editor->deselectAll();
}
MoveToLayerCommand::~MoveToLayerCommand() {
if (createNewLayer) {
delete createNewLayer;
}
}
std::string MoveToLayerCommand::describe() {
const size_t number_of_objects = objects.size();
/// TRANSLATORS: Context: Undo/Redo ...
return tfm::format(ngettext("Move %d object from layer %s to layer %s", "Move %d objects from layer %s to layer %s", number_of_objects).c_str(),
number_of_objects, oldName, newName);
}
void MoveToLayerCommand::removeGameObject() {
// Remove objects
for (int index = 0; index < (int)objects.size(); index++) {
Scenery *scenery = objects[index];
//Now we remove the object from scenery.
for (auto it = editor->sceneryLayers.begin(); it != editor->sceneryLayers.end(); ++it){
auto it2 = find(it->second->objects.begin(), it->second->objects.end(), scenery);
if (it2 != it->second->objects.end()) {
it->second->objects.erase(it2);
break;
}
}
}
}
void MoveToLayerCommand::addGameObject(const std::string& layer) {
// Add objects
const auto& target = editor->sceneryLayers[layer];
for (int index = 0; index < (int)objects.size(); index++) {
target->objects.push_back(objects[index]);
}
}
diff --git a/src/Functions.cpp b/src/Functions.cpp
index 6735cdc..9e171a1 100644
--- a/src/Functions.cpp
+++ b/src/Functions.cpp
@@ -1,1620 +1,1636 @@
/*
* Copyright (C) 2011-2013 Me and My Shadow
*
* This file is part of Me and My Shadow.
*
* Me and My Shadow is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Me and My Shadow is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Me and My Shadow. If not, see <http://www.gnu.org/licenses/>.
*/
#include <stdio.h>
#include <math.h>
#include <locale.h>
#include <algorithm>
#include <SDL.h>
#include <SDL_mixer.h>
#include <SDL_syswm.h>
#include <SDL_ttf.h>
#include <string>
#include "Globals.h"
#include "Functions.h"
#include "FileManager.h"
#include "GameObjects.h"
#include "LevelPack.h"
#include "TitleMenu.h"
#include "OptionsMenu.h"
#include "CreditsMenu.h"
#include "LevelEditSelect.h"
#include "LevelEditor.h"
#include "Game.h"
#include "LevelPlaySelect.h"
#include "Addons.h"
#include "InputManager.h"
#include "ImageManager.h"
#include "MusicManager.h"
#include "SoundManager.h"
#include "ScriptExecutor.h"
#include "LevelPackManager.h"
#include "ThemeManager.h"
#include "GUIListBox.h"
#include "GUIOverlay.h"
#include "StatisticsManager.h"
#include "StatisticsScreen.h"
#include "Cursors.h"
#include "ScriptAPI.h"
#include "libs/tinyformat/tinyformat.h"
#include "libs/tinygettext/tinygettext.hpp"
#include "libs/tinygettext/log.hpp"
#include "libs/findlocale/findlocale.h"
using namespace std;
#ifdef WIN32
#include <windows.h>
#include <shellapi.h>
#include <shlobj.h>
#define TO_UTF8(SRC, DEST) WideCharToMultiByte(CP_UTF8, 0, SRC, -1, DEST, sizeof(DEST), NULL, NULL)
#define TO_UTF16(SRC, DEST) MultiByteToWideChar(CP_UTF8, 0, SRC, -1, DEST, sizeof(DEST)/sizeof(DEST[0]))
#else
#include <strings.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <dirent.h>
#endif
//Initialise the musicManager.
//The MusicManager is used to prevent loading music files multiple times and for playing/fading music.
MusicManager musicManager;
//Initialise the soundManager.
//The SoundManager is used to keep track of the sfx in the game.
SoundManager soundManager;
//Initialise the levelPackManager.
//The LevelPackManager is used to prevent loading levelpacks multiple times and for the game to know which levelpacks there are.
LevelPackManager levelPackManager;
//Map containing changed settings using command line arguments.
map<string,string> tmpSettings;
//Pointer to the settings object.
//It is used to load and save the settings file and change the settings.
Settings* settings=nullptr;
SDL_Renderer* sdlRenderer=nullptr;
std::string ngettext(const std::string& message,const std::string& messageplural,int num) {
if (dictionaryManager) {
return dictionaryManager->get_dictionary().translate_plural(message, messageplural, num);
} else {
//Assume it's of English plural rule
return (num != 1) ? messageplural : message;
}
}
void applySurface(int x,int y,SDL_Surface* source,SDL_Surface* dest,SDL_Rect* clip){
//The offset is needed to draw at the right location.
SDL_Rect offset;
offset.x=x;
offset.y=y;
//Let SDL do the drawing of the surface.
SDL_BlitSurface(source,clip,dest,&offset);
}
void drawRect(int x,int y,int w,int h,SDL_Renderer& renderer,Uint32 color){
//NOTE: We let SDL_gfx render it.
SDL_SetRenderDrawColor(&renderer,color >> 24,color >> 16,color >> 8,255);
//rectangleRGBA(&renderer,x,y,x+w,y+h,color >> 24,color >> 16,color >> 8,255);
const SDL_Rect r{x,y,w,h};
SDL_RenderDrawRect(&renderer,&r);
}
//Draw a box with anti-aliased borders using SDL_gfx.
void drawGUIBox(int x,int y,int w,int h,SDL_Renderer& renderer,Uint32 color){
SDL_Renderer* rd = &renderer;
//FIXME, this may get the wrong color on system with different endianness.
//Fill content's background color from function parameter
SDL_SetRenderDrawColor(rd,color >> 24,color >> 16,color >> 8,color >> 0);
{
const SDL_Rect r{x+1,y+1,w-2,h-2};
SDL_RenderFillRect(rd, &r);
}
SDL_SetRenderDrawColor(rd,0,0,0,255);
//Draw first black borders around content and leave 1 pixel in every corner
SDL_RenderDrawLine(rd,x+1,y,x+w-2,y);
SDL_RenderDrawLine(rd,x+1,y+h-1,x+w-2,y+h-1);
SDL_RenderDrawLine(rd,x,y+1,x,y+h-2);
SDL_RenderDrawLine(rd,x+w-1,y+1,x+w-1,y+h-2);
//Fill the corners with transperent color to create anti-aliased borders
SDL_SetRenderDrawColor(rd,0,0,0,160);
SDL_RenderDrawPoint(rd,x,y);
SDL_RenderDrawPoint(rd,x,y+h-1);
SDL_RenderDrawPoint(rd,x+w-1,y);
SDL_RenderDrawPoint(rd,x+w-1,y+h-1);
//Draw second lighter border around content
SDL_SetRenderDrawColor(rd,0,0,0,64);
{
const SDL_Rect r{x+1,y+1,w-2,h-2};
SDL_RenderDrawRect(rd,&r);
}
SDL_SetRenderDrawColor(rd,0,0,0,50);
//Create anti-aliasing in corners of second border
SDL_RenderDrawPoint(rd,x+1,y+1);
SDL_RenderDrawPoint(rd,x+1,y+h-2);
SDL_RenderDrawPoint(rd,x+w-2,y+1);
SDL_RenderDrawPoint(rd,x+w-2,y+h-2);
}
void drawLine(int x1,int y1,int x2,int y2,SDL_Renderer& renderer,Uint32 color){
SDL_SetRenderDrawColor(&renderer,color >> 24,color >> 16,color >> 8,255);
//NOTE: We let SDL_gfx render it.
//lineRGBA(&renderer,x1,y1,x2,y2,color >> 24,color >> 16,color >> 8,255);
SDL_RenderDrawLine(&renderer,x1,y1,x2,y2);
}
void drawLineWithArrow(int x1,int y1,int x2,int y2,SDL_Renderer& renderer,Uint32 color,int spacing,int offset,int xsize,int ysize){
//Draw line first
drawLine(x1,y1,x2,y2,renderer,color);
//calc delta and length
double dx=x2-x1;
double dy=y2-y1;
double length=sqrt(dx*dx+dy*dy);
if(length<0.001) return;
//calc the unit vector
dx/=length; dy/=length;
//Now draw arrows on it
for(double p=offset;p<length;p+=spacing){
drawLine(int(x1+p*dx+0.5),int(y1+p*dy+0.5),
int(x1+(p-xsize)*dx-ysize*dy+0.5),int(y1+(p-xsize)*dy+ysize*dx+0.5),renderer,color);
drawLine(int(x1+p*dx+0.5),int(y1+p*dy+0.5),
int(x1+(p-xsize)*dx+ysize*dy+0.5),int(y1+(p-xsize)*dy-ysize*dx+0.5),renderer,color);
}
}
ScreenData creationFailed() {
return ScreenData{ nullptr };
}
ScreenData createScreen(){
//Check if we are going fullscreen.
if(settings->getBoolValue("fullscreen"))
pickFullscreenResolution();
//Set the screen_width and height.
SCREEN_WIDTH=atoi(settings->getValue("width").c_str());
SCREEN_HEIGHT=atoi(settings->getValue("height").c_str());
//Update the camera.
camera.w=SCREEN_WIDTH;
camera.h=SCREEN_HEIGHT;
//Set the flags.
Uint32 flags = 0;
Uint32 currentFlags = SDL_GetWindowFlags(sdlWindow);
//#if !defined(ANDROID)
// flags |= SDL_DOUBLEBUF;
//#endif
if(settings->getBoolValue("fullscreen")) {
flags|=SDL_WINDOW_FULLSCREEN; //TODO with SDL2 we can also do SDL_WINDOW_FULLSCREEN_DESKTOP
}
else if(settings->getBoolValue("resizable"))
flags|=SDL_WINDOW_RESIZABLE;
//Create the window and renderer if they don't exist and check if there weren't any errors.
if (!sdlWindow && !sdlRenderer) {
SDL_CreateWindowAndRenderer(SCREEN_WIDTH, SCREEN_HEIGHT, flags, &sdlWindow, &sdlRenderer);
if(!sdlWindow || !sdlRenderer){
std::cerr << "FATAL ERROR: SDL_CreateWindowAndRenderer failed.\nError: " << SDL_GetError() << std::endl;
return creationFailed();
}
SDL_SetRenderDrawBlendMode(sdlRenderer, SDL_BlendMode::SDL_BLENDMODE_BLEND);
// White background so we see the menu on failure.
SDL_SetRenderDrawColor(sdlRenderer, 255, 255, 255, 255);
} else if (sdlWindow) {
// Try changing to/from fullscreen
if(SDL_SetWindowFullscreen(sdlWindow, flags & SDL_WINDOW_FULLSCREEN) != 0) {
std::cerr << "WARNING: Failed to switch to fullscreen: " << SDL_GetError() << std::endl;
};
currentFlags = SDL_GetWindowFlags(sdlWindow);
// Change fullscreen resolution
if((currentFlags & SDL_WINDOW_FULLSCREEN ) || (currentFlags & SDL_WINDOW_FULLSCREEN_DESKTOP)) {
SDL_DisplayMode m{0,0,0,0,nullptr};
SDL_GetWindowDisplayMode(sdlWindow,&m);
m.w = SCREEN_WIDTH;
m.h = SCREEN_HEIGHT;
if(SDL_SetWindowDisplayMode(sdlWindow, &m) != 0) {
std::cerr << "WARNING: Failed to set display mode: " << SDL_GetError() << std::endl;
}
} else {
SDL_SetWindowSize(sdlWindow, SCREEN_WIDTH, SCREEN_HEIGHT);
}
}
//Now configure the newly created window (if windowed).
if(settings->getBoolValue("fullscreen")==false)
configureWindow();
//Set the the window caption.
SDL_SetWindowTitle(sdlWindow, ("Me and My Shadow "+version).c_str());
//FIXME Seems to be obsolete
// SDL_EnableUNICODE(1);
//Nothing went wrong so return true.
return ScreenData{sdlRenderer};
}
-vector<_res> getResolutionList(){
+vector<SDL_Point> getResolutionList(){
//Vector that will hold the resolutions to choose from.
- vector<_res> resolutionList;
+ vector<SDL_Point> resolutionList;
//Enumerate available resolutions using SDL_ListModes()
//NOTE: we enumerate fullscreen resolutions because
// windowed resolutions always can be arbitrary
if(resolutionList.empty()){
// SDL_Rect **modes=SDL_ListModes(NULL,SDL_FULLSCREEN|SCREEN_FLAGS|SDL_ANYFORMAT);
//NOTe - currently only using the first display (0)
int numDisplayModes = SDL_GetNumDisplayModes(0);
if(numDisplayModes < 1){
cerr<<"ERROR: Can't enumerate available screen resolutions."
" Use predefined screen resolutions list instead."<<endl;
- static const _res predefinedResolutionList[] = {
+ static const SDL_Point predefinedResolutionList[] = {
{800,600},
{1024,600},
{1024,768},
{1152,864},
{1280,720},
{1280,768},
{1280,800},
{1280,960},
{1280,1024},
{1360,768},
{1366,768},
{1440,900},
{1600,900},
{1600,1200},
{1680,1080},
{1920,1080},
{1920,1200},
{2560,1440},
{3840,2160}
};
//Fill the resolutionList.
- for(unsigned int i=0;i<sizeof(predefinedResolutionList)/sizeof(_res);i++){
+ for (unsigned int i = 0; i<sizeof(predefinedResolutionList) / sizeof(SDL_Point); i++){
resolutionList.push_back(predefinedResolutionList[i]);
}
}else{
//Fill the resolutionList.
for(int i=0;i < numDisplayModes; ++i){
SDL_DisplayMode mode;
int error = SDL_GetDisplayMode(0, i, &mode);
if(error < 0) {
//We failed to get a display mode. Should we crash here?
std::cerr << "ERROR: Failed to get display mode " << i << " " << std::endl;
}
//Check if the resolution is higher than the minimum (800x600).
if(mode.w >= 800 && mode.h >= 600){
- _res res={mode.w, mode.h};
+ SDL_Point res = { mode.w, mode.h };
resolutionList.push_back(res);
}
}
//Reverse it so that we begin with the lowest resolution.
reverse(resolutionList.begin(),resolutionList.end());
}
}
//Return the resolution list.
return resolutionList;
}
void pickFullscreenResolution(){
//Get the resolution list.
- vector<_res> resolutionList=getResolutionList();
-
+ vector<SDL_Point> resolutionList=getResolutionList();
+
//The resolution that will hold the final result, we start with the minimum (800x600).
- _res closestMatch={800,600};
+ SDL_Point closestMatch = { 800, 600 };
int width=atoi(getSettings()->getValue("width").c_str());
- //int height=atoi(getSettings()->getValue("height").c_str());
-
+ int height=atoi(getSettings()->getValue("height").c_str());
+ int delta = 0x40000000;
+
//Now loop through the resolutionList.
- for(int i=0;i<(int)resolutionList.size();i++){
- //The delta between the closestMatch and the resolution from the list.
- int dM=(closestMatch.w-resolutionList[i].w);
- //The delta between the target width and the resolution from the list.
- int dT=(width-resolutionList[i].w);
-
- //Since the resolutions are getting higher the lower (more negative) the further away it is.
- //That's why we check if the deltaMatch is lower than the the deltaTarget.
- if((dM)<(dT)){
- closestMatch.w=resolutionList[i].w;
- closestMatch.h=resolutionList[i].h;
- }
+ for (int i = 0; i < (int)resolutionList.size(); i++){
+ int dx = width - resolutionList[i].x;
+ if (dx < 0) dx = -dx;
+ int dy = height - resolutionList[i].y;
+ if (dy < 0) dy = -dy;
+
+ if (dx + dy < delta){
+ delta = dx + dy;
+ closestMatch.x = resolutionList[i].x;
+ closestMatch.y = resolutionList[i].y;
+ }
}
-
+
//Now set the resolution to the closest match.
char s[64];
- sprintf(s,"%d",closestMatch.w);
+ sprintf(s,"%d",closestMatch.x);
getSettings()->setValue("width",s);
- sprintf(s,"%d",closestMatch.h);
+ sprintf(s,"%d",closestMatch.y);
getSettings()->setValue("height",s);
}
void configureWindow(){
//We only need to configure the window if it's resizable.
if(!getSettings()->getBoolValue("resizable"))
return;
//We use a new function in SDL2 to restrict minimum window size
SDL_SetWindowMinimumSize(sdlWindow, 800, 600);
}
void onVideoResize(ImageManager& imageManager, SDL_Renderer &renderer){
//Check if the resize event isn't malformed.
if(event.window.data1<=0 || event.window.data2<=0)
return;
//Check the size limit.
//TODO: SDL2 porting note: This may break on systems non-X11 or Windows systems as the window size won't be limited
//there.
if(event.window.data1<800)
event.window.data1=800;
if(event.window.data2<600)
event.window.data2=600;
//Check if it really resizes.
if(SCREEN_WIDTH==event.window.data1 && SCREEN_HEIGHT==event.window.data2)
return;
char s[32];
//Set the new width and height.
SDL_snprintf(s,32,"%d",event.window.data1);
getSettings()->setValue("width",s);
SDL_snprintf(s,32,"%d",event.window.data2);
getSettings()->setValue("height",s);
//FIXME: THIS doesn't work properly.
//Do resizing.
SCREEN_WIDTH = event.window.data1;
SCREEN_HEIGHT = event.window.data2;
//Update the camera.
camera.w=SCREEN_WIDTH;
camera.h=SCREEN_HEIGHT;
//Tell the theme to resize.
if(!loadTheme(imageManager,renderer,""))
return;
//And let the currentState update it's GUI to the new resolution.
currentState->resize(imageManager, renderer);
}
ScreenData init(){
//Initialze SDL.
- if(SDL_Init(SDL_INIT_EVERYTHING)==-1) {
+ if(SDL_Init(SDL_INIT_TIMER|SDL_INIT_VIDEO|SDL_INIT_AUDIO|SDL_INIT_JOYSTICK)==-1) {
std::cerr << "FATAL ERROR: SDL_Init failed\nError: " << SDL_GetError() << std::endl;
return creationFailed();
}
//Initialze SDL_mixer (audio).
//Note for SDL2 port: Changed frequency from 22050 to 44100.
//22050 caused some sound artifacts on my system, and I'm not sure
//why one would use it in this day and age anyhow.
//unless it's for compatability with some legacy system.
if(Mix_OpenAudio(44100,MIX_DEFAULT_FORMAT,2,1024)==-1){
std::cerr << "FATAL ERROR: Mix_OpenAudio failed\nError: " << Mix_GetError() << std::endl;
return creationFailed();
}
//Set the volume.
Mix_Volume(-1,atoi(settings->getValue("sound").c_str()));
//Increase the number of channels.
soundManager.setNumberOfChannels(48);
//Initialze SDL_ttf (fonts).
if(TTF_Init()==-1){
std::cerr << "FATAL ERROR: TTF_Init failed\nError: " << TTF_GetError() << std::endl;
return creationFailed();
}
//Create the screen.
ScreenData screenData(createScreen());
if(!screenData) {
return creationFailed();
}
//Load key config. Then initialize joystick support.
inputMgr.loadConfig();
inputMgr.openAllJoysitcks();
//Init tinygettext for translations for the right language
dictionaryManager = new tinygettext::DictionaryManager();
dictionaryManager->set_use_fuzzy(false);
dictionaryManager->add_directory(getDataPath()+"locale");
dictionaryManager->set_charset("UTF-8");
//Check if user have defined own language. If not, find it out for the player using findlocale
string lang=getSettings()->getValue("lang");
if(lang.length()>0){
printf("Locale set by user to %s\n",lang.c_str());
language=lang;
}else{
FL_Locale *locale;
FL_FindLocale(&locale,FL_MESSAGES);
printf("Locale isn't set by user: %s\n",locale->lang);
language=locale->lang;
if(locale->country!=NULL){
language+=string("_")+string(locale->country);
}
if(locale->variant!=NULL){
language+=string("@")+string(locale->variant);
}
FL_FreeLocale(&locale);
}
//Now set the language in the dictionaryManager.
dictionaryManager->set_language(tinygettext::Language::from_name(language));
+#ifdef WIN32
+ //Some ad-hoc fix for Windows since it accepts "zh-CN" but not "zh_CN"
+ std::string language2;
+ for (auto c : language) {
+ if (isalnum(c)) language2.push_back(c);
+ else if (c == '_') language2.push_back('-');
+ else break;
+ }
+ const char* languagePtr = language2.c_str();
+#else
+ const char* languagePtr = language.c_str();
+#endif
+
+ //Also set the language for tinyformat.
+ tfm::setLocale(languagePtr);
+
+ //Set time format.
+ setlocale(LC_TIME, languagePtr);
+
//Disable annoying 'Couldn't translate: blah blah blah'
tinygettext::Log::set_log_info_callback(NULL);
-
- //Set time format to the user-preference of the system.
- setlocale(LC_TIME,"");
//Create the types of blocks.
for(int i=0;i<TYPE_MAX;i++){
Game::blockNameMap[Game::blockName[i]]=i;
}
//Structure that holds the event type/name pair.
struct EventTypeName{
int type;
const char* name;
};
//Create the types of game object event types.
{
const EventTypeName types[]={
{GameObjectEvent_PlayerWalkOn,"playerWalkOn"},
{GameObjectEvent_PlayerIsOn,"playerIsOn"},
{GameObjectEvent_PlayerLeave,"playerLeave"},
{GameObjectEvent_OnCreate,"onCreate"},
{GameObjectEvent_OnEnterFrame,"onEnterFrame"},
{ GameObjectEvent_OnPlayerInteraction, "onPlayerInteraction" },
{GameObjectEvent_OnToggle,"onToggle"},
{GameObjectEvent_OnSwitchOn,"onSwitchOn"},
{GameObjectEvent_OnSwitchOff,"onSwitchOff"},
{0,NULL}
};
for(int i=0;types[i].name;i++){
Game::gameObjectEventNameMap[types[i].name]=types[i].type;
Game::gameObjectEventTypeMap[types[i].type]=types[i].name;
}
}
//Create the types of level event types.
{
const EventTypeName types[]={
{LevelEvent_OnCreate,"onCreate"},
{LevelEvent_OnSave,"onSave"},
{LevelEvent_OnLoad,"onLoad"},
{0,NULL}
};
for(int i=0;types[i].name;i++){
Game::levelEventNameMap[types[i].name]=types[i].type;
Game::levelEventTypeMap[types[i].type]=types[i].name;
}
}
//Nothing went wrong so we return true.
return screenData;
}
static TTF_Font* loadFont(const char* name,int size){
TTF_Font* tmpFont=TTF_OpenFont((getDataPath()+"font/"+name+".ttf").c_str(),size);
if(tmpFont){
return tmpFont;
}else{
#if defined(ANDROID)
//Android has built-in DroidSansFallback.ttf. (?)
return TTF_OpenFont("/system/fonts/DroidSansFallback.ttf",size);
#else
return TTF_OpenFont((getDataPath()+"font/DroidSansFallback.ttf").c_str(),size);
#endif
}
}
bool loadFonts(){
//Load the fonts.
//NOTE: This is a separate method because it will be called separately when re-initing in case of language change.
//First close the fonts if needed.
if(fontTitle)
TTF_CloseFont(fontTitle);
if(fontGUI)
TTF_CloseFont(fontGUI);
if(fontGUISmall)
TTF_CloseFont(fontGUISmall);
if(fontText)
TTF_CloseFont(fontText);
if(fontMono)
TTF_CloseFont(fontMono);
/// TRANSLATORS: Font used in GUI:
/// - Use "knewave" for languages using Latin and Latin-derived alphabets
/// - "DroidSansFallback" can be used for non-Latin writing systems
fontTitle=loadFont(_("knewave"),55);
fontGUI=loadFont(_("knewave"),32);
fontGUISmall=loadFont(_("knewave"),24);
/// TRANSLATORS: Font used for normal text:
/// - Use "Blokletters-Viltstift" for languages using Latin and Latin-derived alphabets
/// - "DroidSansFallback" can be used for non-Latin writing systems
fontText=loadFont(_("Blokletters-Viltstift"),16);
fontMono=loadFont("DejaVuSansMono",12);
if(fontTitle==NULL || fontGUI==NULL || fontGUISmall==NULL || fontText==NULL || fontMono==NULL){
printf("ERROR: Unable to load fonts! \n");
return false;
}
//Nothing went wrong so return true.
return true;
}
//Generate small arrows used for some GUI widgets.
static void generateArrows(SDL_Renderer& renderer){
TTF_Font* fontArrow=loadFont(_("knewave"),18);
arrowLeft1=textureFromText(renderer,*fontArrow,"<",objThemes.getTextColor(false));
arrowRight1=textureFromText(renderer,*fontArrow,">",objThemes.getTextColor(false));
arrowLeft2=textureFromText(renderer,*fontArrow,"<",objThemes.getTextColor(true));
arrowRight2=textureFromText(renderer,*fontArrow,">",objThemes.getTextColor(true));
TTF_CloseFont(fontArrow);
}
bool loadTheme(ImageManager& imageManager,SDL_Renderer& renderer,std::string name){
//Load default fallback theme if it isn't loaded yet
if(objThemes.themeCount()==0){
if(objThemes.appendThemeFromFile(getDataPath()+"themes/Cloudscape/theme.mnmstheme", imageManager, renderer)==NULL){
printf("ERROR: Can't load default theme file\n");
return false;
}
}
//Resize background or load specific theme
bool success=true;
if(name==""||name.empty()){
objThemes.scaleToScreen();
}else{
string theme=processFileName(name);
if(objThemes.appendThemeFromFile(theme+"/theme.mnmstheme", imageManager, renderer)==NULL){
printf("ERROR: Can't load theme %s\n",theme.c_str());
success=false;
}
}
generateArrows(renderer);
//Everything went fine so return true.
return success;
}
static SDL_Cursor* loadCursor(const char* image[]){
int i,row,col;
//The array that holds the data (0=white 1=black)
Uint8 data[4*32];
//The array that holds the alpha mask (0=transparent 1=visible)
Uint8 mask[4*32];
//The coordinates of the hotspot of the cursor.
int hotspotX, hotspotY;
i=-1;
//Loop through the rows and columns.
//NOTE: We assume a cursor size of 32x32.
for(row=0;row<32;++row){
for(col=0; col<32;++col){
if(col % 8) {
data[i]<<=1;
mask[i]<<=1;
}else{
++i;
data[i]=mask[i]=0;
}
switch(image[4+row][col]){
case '+':
data[i] |= 0x01;
mask[i] |= 0x01;
break;
case '.':
mask[i] |= 0x01;
break;
default:
break;
}
}
}
//Get the hotspot x and y locations from the last line of the cursor.
sscanf(image[4+row],"%d,%d",&hotspotX,&hotspotY);
return SDL_CreateCursor(data,mask,32,32,hotspotX,hotspotY);
}
bool loadFiles(ImageManager& imageManager, SDL_Renderer& renderer){
//Load the fonts.
if(!loadFonts())
return false;
//Show a loading screen
{
int w = 0,h = 0;
SDL_GetRendererOutputSize(&renderer, &w, &h);
SDL_Color fg={255,255,255,0};
TexturePtr loadingTexture = titleTextureFromText(renderer, _("Loading..."), fg, w);
SDL_Rect loadingRect = rectFromTexture(*loadingTexture);
loadingRect.x = (w-loadingRect.w)/2;
loadingRect.y = (h-loadingRect.h)/2;
SDL_RenderCopy(sdlRenderer, loadingTexture.get(), NULL, &loadingRect);
SDL_RenderPresent(sdlRenderer);
SDL_RenderClear(sdlRenderer);
}
musicManager.destroy();
//Load the music and play it.
if(musicManager.loadMusic((getDataPath()+"music/menu.music")).empty()){
printf("WARNING: Unable to load background music! \n");
}
musicManager.playMusic("menu",false);
//Load all the music lists from the data and user data path.
{
vector<string> musicLists=enumAllFiles((getDataPath()+"music/"),"list",true);
for(unsigned int i=0;i<musicLists.size();i++)
getMusicManager()->loadMusicList(musicLists[i]);
musicLists=enumAllFiles((getUserPath(USER_DATA)+"music/"),"list",true);
for(unsigned int i=0;i<musicLists.size();i++)
getMusicManager()->loadMusicList(musicLists[i]);
}
//Set the list to the configured one.
getMusicManager()->setMusicList(getSettings()->getValue("musiclist"));
//Check if music is enabled.
if(getSettings()->getBoolValue("music"))
getMusicManager()->setEnabled();
//Load the sound effects
soundManager.loadSound((getDataPath()+"sfx/jump.wav").c_str(),"jump");
soundManager.loadSound((getDataPath()+"sfx/hit.wav").c_str(),"hit");
soundManager.loadSound((getDataPath()+"sfx/checkpoint.wav").c_str(),"checkpoint");
soundManager.loadSound((getDataPath()+"sfx/swap.wav").c_str(),"swap");
soundManager.loadSound((getDataPath()+"sfx/toggle.ogg").c_str(),"toggle");
soundManager.loadSound((getDataPath()+"sfx/error.wav").c_str(),"error");
soundManager.loadSound((getDataPath()+"sfx/collect.wav").c_str(),"collect");
soundManager.loadSound((getDataPath()+"sfx/achievement.ogg").c_str(),"achievement");
//Load the cursor images from the Cursor.h file.
cursors[CURSOR_POINTER]=loadCursor(pointer);
cursors[CURSOR_CARROT]=loadCursor(ibeam);
cursors[CURSOR_DRAG]=loadCursor(closedhand);
cursors[CURSOR_SIZE_HOR]=loadCursor(size_hor);
cursors[CURSOR_SIZE_VER]=loadCursor(size_ver);
cursors[CURSOR_SIZE_FDIAG]=loadCursor(size_fdiag);
cursors[CURSOR_SIZE_BDIAG]=loadCursor(size_bdiag);
cursors[CURSOR_REMOVE]=loadCursor(remove_cursor);
cursors[CURSOR_POINTING_HAND] = loadCursor(pointing_hand);
//Set the default cursor right now.
SDL_SetCursor(cursors[CURSOR_POINTER]);
levelPackManager.destroy();
//Now sum up all the levelpacks.
vector<string> v=enumAllDirs(getDataPath()+"levelpacks/");
for(vector<string>::iterator i=v.begin(); i!=v.end(); ++i){
levelPackManager.loadLevelPack(getDataPath()+"levelpacks/"+*i);
}
v=enumAllDirs(getUserPath(USER_DATA)+"levelpacks/");
for(vector<string>::iterator i=v.begin(); i!=v.end(); ++i){
levelPackManager.loadLevelPack(getUserPath(USER_DATA)+"levelpacks/"+*i);
}
v=enumAllDirs(getUserPath(USER_DATA)+"custom/levelpacks/");
for(vector<string>::iterator i=v.begin(); i!=v.end(); ++i){
levelPackManager.loadLevelPack(getUserPath(USER_DATA)+"custom/levelpacks/"+*i);
}
//Now we add a special levelpack that will contain the levels not in a levelpack.
LevelPack* levelsPack=new LevelPack;
levelsPack->levelpackName="Levels";
levelsPack->levelpackPath=LEVELS_PATH;
levelsPack->type=COLLECTION;
LevelPack* customLevelsPack=new LevelPack;
customLevelsPack->levelpackName="Custom Levels";
customLevelsPack->levelpackPath=CUSTOM_LEVELS_PATH;
customLevelsPack->type=COLLECTION;
//List the main levels and add them one for one.
v = enumAllFiles(getDataPath() + "levels/");
for (vector<string>::iterator i = v.begin(); i != v.end(); ++i){
levelsPack->addLevel(getDataPath() + "levels/" + *i);
levelsPack->setLocked(levelsPack->getLevelCount() - 1);
}
//List the addon levels and add them one for one.
v=enumAllFiles(getUserPath(USER_DATA)+"levels/");
for(vector<string>::iterator i=v.begin(); i!=v.end(); ++i){
levelsPack->addLevel(getUserPath(USER_DATA)+"levels/"+*i);
levelsPack->setLocked(levelsPack->getLevelCount()-1);
}
//List the custom levels and add them one for one.
v=enumAllFiles(getUserPath(USER_DATA)+"custom/levels/");
for(vector<string>::iterator i=v.begin(); i!=v.end(); ++i){
levelsPack->addLevel(getUserPath(USER_DATA)+"custom/levels/"+*i);
levelsPack->setLocked(levelsPack->getLevelCount()-1);
customLevelsPack->addLevel(getUserPath(USER_DATA)+"custom/levels/"+*i);
customLevelsPack->setLocked(customLevelsPack->getLevelCount()-1);
}
//Add them to the manager.
levelPackManager.addLevelPack(levelsPack);
levelPackManager.addLevelPack(customLevelsPack);
//Load statistics
statsMgr.loadPicture(renderer, imageManager);
statsMgr.registerAchievements(imageManager);
statsMgr.loadFile(getUserPath(USER_CONFIG)+"statistics");
//Do something ugly and slow
statsMgr.reloadCompletedLevelsAndAchievements();
statsMgr.reloadOtherAchievements();
//Load the theme, both menu and default.
//NOTE: Loading theme may fail and returning false would stop everything, default theme will be used instead.
if (!loadTheme(imageManager,renderer,getSettings()->getValue("theme"))){
getSettings()->setValue("theme","%DATA%/themes/Cloudscape");
saveSettings();
}
//Nothing failed so return true.
return true;
}
bool loadSettings(){
//Check the version of config file.
int version = 0;
std::string cfgV05 = getUserPath(USER_CONFIG) + "meandmyshadow_V0.5.cfg";
std::string cfgV04 = getUserPath(USER_CONFIG) + "meandmyshadow.cfg";
if (fileExists(cfgV05.c_str())) {
//We find a config file of current version.
version = 0x000500;
} else if (fileExists(cfgV04.c_str())) {
//We find a config file of V0.4 version or earlier.
copyFile(cfgV04.c_str(), cfgV05.c_str());
version = 0x000400;
} else {
//No config file found, just create a new one.
version = 0x000500;
}
settings=new Settings(cfgV05);
settings->parseFile(version);
//Now apply settings changed through command line arguments, if any.
map<string,string>::iterator it;
for(it=tmpSettings.begin();it!=tmpSettings.end();++it){
settings->setValue(it->first,it->second);
}
tmpSettings.clear();
//Always return true?
return true;
}
bool saveSettings(){
return settings->save();
}
Settings* getSettings(){
return settings;
}
MusicManager* getMusicManager(){
return &musicManager;
}
SoundManager* getSoundManager(){
return &soundManager;
}
LevelPackManager* getLevelPackManager(){
return &levelPackManager;
}
void flipScreen(SDL_Renderer& renderer){
// Render the data from the back buffer.
SDL_RenderPresent(&renderer);
}
void clean(){
//Save statistics
statsMgr.saveFile(getUserPath(USER_CONFIG)+"statistics");
//We delete the settings.
if(settings){
delete settings;
settings=NULL;
}
//Delete dictionaryManager.
delete dictionaryManager;
//Get rid of the currentstate.
//NOTE: The state is probably already deleted by the changeState function.
if(currentState)
delete currentState;
//Destroy the GUI if present.
if(GUIObjectRoot){
delete GUIObjectRoot;
GUIObjectRoot=NULL;
}
//These calls to destroy makes sure stuff is
//deleted before SDL is uninitialised (as these managers are stack allocated
//globals.)
//Destroy the musicManager.
musicManager.destroy();
//Destroy all sounds
soundManager.destroy();
//Destroy the cursors.
for(int i=0;i<CURSOR_MAX;i++){
SDL_FreeCursor(cursors[i]);
cursors[i]=NULL;
}
//Destroy the levelPackManager.
levelPackManager.destroy();
levels=NULL;
//Close all joysticks.
inputMgr.closeAllJoysticks();
//Close the fonts and quit SDL_ttf.
TTF_CloseFont(fontTitle);
TTF_CloseFont(fontGUI);
TTF_CloseFont(fontGUISmall);
TTF_CloseFont(fontText);
TTF_CloseFont(fontMono);
TTF_Quit();
//Remove the temp surface.
SDL_DestroyRenderer(sdlRenderer);
SDL_DestroyWindow(sdlWindow);
arrowLeft1.reset(nullptr);
arrowLeft2.reset(nullptr);
arrowRight1.reset(nullptr);
arrowRight2.reset(nullptr);
//Stop audio.and quit
Mix_CloseAudio();
//SDL2 porting note. Not sure why this was only done on apple.
//#ifndef __APPLE__
Mix_Quit();
//#endif
//And finally quit SDL.
SDL_Quit();
}
void setNextState(int newstate){
//Only change the state when we aren't already exiting.
if(nextState!=STATE_EXIT){
nextState=newstate;
}
}
void changeState(ImageManager& imageManager, SDL_Renderer& renderer, int fade){
//Check if there's a nextState.
if(nextState!=STATE_NULL){
//Fade out, if fading is enabled.
if (currentState && settings->getBoolValue("fading")) {
for (; fade >= 0; fade -= 17) {
currentState->render(imageManager, renderer);
//TODO: Shouldn't the gamestate take care of rendering the GUI?
if (GUIObjectRoot) GUIObjectRoot->render(renderer);
dimScreen(renderer, static_cast<Uint8>(255 - fade));
//draw new achievements (if any) as overlay
statsMgr.render(imageManager, renderer);
flipScreen(renderer);
SDL_Delay(1000/FPS);
}
}
//Delete the currentState.
delete currentState;
currentState=NULL;
//Set the currentState to the nextState.
stateID=nextState;
nextState=STATE_NULL;
//Init the state.
switch(stateID){
case STATE_GAME:
{
currentState=NULL;
Game* game=new Game(renderer, imageManager);
currentState=game;
//Check if we should load record file or a level.
if(!Game::recordFile.empty()){
game->loadRecord(imageManager,renderer,Game::recordFile.c_str());
Game::recordFile.clear();
}else{
game->loadLevel(imageManager,renderer,levels->getLevelFile());
levels->saveLevelProgress();
}
}
break;
case STATE_MENU:
currentState=new Menu(imageManager, renderer);
break;
case STATE_LEVEL_SELECT:
currentState=new LevelPlaySelect(imageManager, renderer);
break;
case STATE_LEVEL_EDIT_SELECT:
currentState=new LevelEditSelect(imageManager, renderer);
break;
case STATE_LEVEL_EDITOR:
{
currentState=NULL;
LevelEditor* levelEditor=new LevelEditor(renderer, imageManager);
currentState=levelEditor;
//Load the selected level.
levelEditor->loadLevel(imageManager,renderer,levels->getLevelFile());
}
break;
case STATE_OPTIONS:
currentState=new Options(imageManager, renderer);
break;
case STATE_ADDONS:
currentState=new Addons(renderer, imageManager);
break;
case STATE_CREDITS:
currentState=new Credits(imageManager,renderer);
break;
case STATE_STATISTICS:
currentState=new StatisticsScreen(imageManager,renderer);
break;
}
//NOTE: STATE_EXIT isn't mentioned, meaning that currentState is null.
//This way the game loop will break and the program will exit.
}
}
void musicStoppedHook(){
//We just call the musicStopped method of the MusicManager.
musicManager.musicStopped();
}
void channelFinishedHook(int channel){
soundManager.channelFinished(channel);
}
bool checkCollision(const SDL_Rect& a,const SDL_Rect& b){
//Check if the left side of box a isn't past the right side of b.
if(a.x>=b.x+b.w){
return false;
}
//Check if the right side of box a isn't left of the left side of b.
if(a.x+a.w<=b.x){
return false;
}
//Check if the top side of box a isn't under the bottom side of b.
if(a.y>=b.y+b.h){
return false;
}
//Check if the bottom side of box a isn't above the top side of b.
if(a.y+a.h<=b.y){
return false;
}
//We have collision.
return true;
}
bool pointOnRect(const SDL_Rect& point, const SDL_Rect& rect) {
if(point.x >= rect.x && point.x < rect.x + rect.w
&& point.y >= rect.y && point.y < rect.y + rect.h) {
return true;
}
return false;
}
int parseArguments(int argc, char** argv){
//Loop through all arguments.
//We start at one since 0 is the command itself.
for(int i=1;i<argc;i++){
string argument=argv[i];
//Check if the argument is the data-dir.
if(argument=="--data-dir"){
//We need a second argument so we increase i.
i++;
if(i>=argc){
printf("ERROR: Missing argument for command '%s'\n\n",argument.c_str());
return -1;
}
//Configure the dataPath with the given path.
dataPath=argv[i];
if(!getDataPath().empty()){
char c=dataPath[dataPath.size()-1];
if(c!='/'&&c!='\\') dataPath+="/";
}
}else if(argument=="--user-dir"){
//We need a second argument so we increase i.
i++;
if(i>=argc){
printf("ERROR: Missing argument for command '%s'\n\n",argument.c_str());
return -1;
}
//Configure the userPath with the given path.
userPath=argv[i];
if(!userPath.empty()){
char c=userPath[userPath.size()-1];
if(c!='/'&&c!='\\') userPath+="/";
}
}else if(argument=="-f" || argument=="-fullscreen" || argument=="--fullscreen"){
tmpSettings["fullscreen"]="1";
}else if(argument=="-w" || argument=="-windowed" || argument=="--windowed"){
tmpSettings["fullscreen"]="0";
}else if(argument=="-mv" || argument=="-music" || argument=="--music"){
//We need a second argument so we increase i.
i++;
if(i>=argc){
printf("ERROR: Missing argument for command '%s'\n\n",argument.c_str());
return -1;
}
//Now set the music volume.
tmpSettings["music"]=argv[i];
}else if(argument=="-sv" || argument=="-sound" || argument=="--sound"){
//We need a second argument so we increase i.
i++;
if(i>=argc){
printf("ERROR: Missing argument for command '%s'\n\n",argument.c_str());
return -1;
}
//Now set sound volume.
tmpSettings["sound"]=argv[i];
}else if(argument=="-set" || argument=="--set"){
//We need a second and a third argument so we increase i.
i+=2;
if(i>=argc){
printf("ERROR: Missing argument for command '%s'\n\n",argument.c_str());
return -1;
}
//And set the setting.
tmpSettings[argv[i-1]]=argv[i];
}else if(argument=="-v" || argument=="-version" || argument=="--version"){
//Print the version.
printf("%s\n",version.c_str());
return 0;
}else if(argument=="-h" || argument=="-help" || argument=="--help"){
//If the help is requested we'll return false without printing an error.
//This way the usage/help text will be printed.
return -1;
}else{
//Any other argument is unknow so we return false.
printf("ERROR: Unknown argument %s\n\n",argument.c_str());
return -1;
}
}
//If everything went well we can return true.
return 1;
}
//Special structure that will recieve the GUIEventCallbacks of the messagebox.
struct msgBoxHandler:public GUIEventCallback{
public:
//Integer containing the ret(urn) value of the messageBox.
int ret;
public:
//Constructor.
msgBoxHandler():ret(0){}
void GUIEventCallback_OnEvent(ImageManager& imageManager, SDL_Renderer& renderer, std::string name,GUIObject* obj,int eventType){
//Make sure it's a click event.
if(eventType==GUIEventClick){
//Set the return value.
ret=obj->value;
//After a click event we can delete the GUI.
if(GUIObjectRoot){
delete GUIObjectRoot;
GUIObjectRoot=NULL;
}
}
}
};
msgBoxResult msgBox(ImageManager& imageManager,SDL_Renderer& renderer, const string& prompt,msgBoxButtons buttons,const string& title){
//Create the event handler.
msgBoxHandler objHandler;
//Create the GUIObjectRoot, the height and y location is temp.
//It depends on the content what it will be.
GUIObject* root=new GUIFrame(imageManager,renderer,(SCREEN_WIDTH-600)/2,200,600,200,title.c_str());
//Integer containing the current y location used to grow dynamic depending on the content.
int y=50;
//Now process the prompt.
{
//NOTE: We shouldn't modify the cotents in the c_str() of a string,
//since it's said that at least in g++ the std::string is copy-on-write
//hence if we modify the content it may break
//The copy of the prompt.
std::vector<char> copyOfPrompt(prompt.begin(), prompt.end());
//Append another '\0' to it.
copyOfPrompt.push_back(0);
//Pointer to the string.
char* lps = &(copyOfPrompt[0]);
//Pointer to a character.
char* lp=NULL;
//The list of labels.
std::vector<GUIObject*> labels;
//We keep looping forever.
//The only way out is with the break statement.
for(;;){
//As long as it's still the same sentence we continue.
//It will stop when there's a newline or end of line.
for(lp=lps;*lp!='\n'&&*lp!='\r'&&*lp!=0;lp++);
//Store the character we stopped on. (End or newline)
char c=*lp;
//Set the character in the string to 0, making lps a string containing one sentence.
*lp=0;
//Add a GUIObjectLabel with the sentence.
GUIObject *label = new GUILabel(imageManager, renderer, 0, y, root->width, 25, lps, 0, true, true, GUIGravityCenter);
labels.push_back(label);
root->addChild(label);
//Calculate the width of the text.
int w = 0;
TTF_SizeUTF8(fontText, lps, &w, NULL);
w += 20;
if (w > root->width) root->width = w;
//Increase y with 25, about the height of the text.
y+=25;
//Check the stored character if it was a stop.
if(c==0){
//It was so break out of the for loop.
lps=lp;
break;
}
//It wasn't meaning more will follow.
//We set lps to point after the "newline" forming a new string.
lps=lp+1;
}
//Shrink the dialog if it's too big.
if (root->width > SCREEN_WIDTH - 20) root->width = SCREEN_WIDTH - 20;
root->left = (SCREEN_WIDTH - root->width) / 2;
//Move labels to their correct locations.
for (auto label : labels) {
label->width = root->width;
}
}
//Add 70 to y to leave some space between the content and the buttons.
y+=70;
//Recalc the size of the message box.
root->top=(SCREEN_HEIGHT-y)/2;
root->height=y;
//Now we need to add the buttons.
//Integer containing the number of buttons to add.
int count=0;
//Array with the return codes for the buttons.
int value[3]={0};
//Array containing the captation for the buttons.
string button[3]={"","",""};
switch(buttons){
case MsgBoxOKCancel:
count=2;
button[0]=_("OK");value[0]=MsgBoxOK;
button[1]=_("Cancel");value[1]=MsgBoxCancel;
break;
case MsgBoxAbortRetryIgnore:
count=3;
button[0]=_("Abort");value[0]=MsgBoxAbort;
button[1]=_("Retry");value[1]=MsgBoxRetry;
button[2]=_("Ignore");value[2]=MsgBoxIgnore;
break;
case MsgBoxYesNoCancel:
count=3;
button[0]=_("Yes");value[0]=MsgBoxYes;
button[1]=_("No");value[1]=MsgBoxNo;
button[2]=_("Cancel");value[2]=MsgBoxCancel;
break;
case MsgBoxYesNo:
count=2;
button[0]=_("Yes");value[0]=MsgBoxYes;
button[1]=_("No");value[1]=MsgBoxNo;
break;
case MsgBoxRetryCancel:
count=2;
button[0]=_("Retry");value[0]=MsgBoxRetry;
button[1]=_("Cancel");value[1]=MsgBoxCancel;
break;
default:
count=1;
button[0]=_("OK");value[0]=MsgBoxOK;
break;
}
//Now we start making the buttons.
{
//Reduce y so that the buttons fit inside the frame.
y-=40;
double places[3]={0.0};
if(count==1){
places[0]=0.5;
}else if(count==2){
places[0]=0.35;
places[1]=0.65;
}else if(count==3){
places[0]=0.25;
places[1]=0.5;
places[2]=0.75;
}
std::vector<GUIButton*> buttons;
//Loop to add the buttons.
for(int i=0;i<count;i++){
GUIButton* obj = new GUIButton(imageManager, renderer, root->width*places[i], y, -1, 36, button[i].c_str(), value[i], true, true, GUIGravityCenter);
obj->eventCallback=&objHandler;
buttons.push_back(obj);
root->addChild(obj);
}
//Update widgets
for (int i = 0; i < count; i++) {
buttons[i]->render(renderer, 0, 0, false);
}
bool overlap = false;
//Check if they overlap
if (buttons[0]->left - buttons[0]->gravityX < 5 ||
buttons[count - 1]->left - buttons[count - 1]->gravityX + buttons[count - 1]->width > root->width - 5)
{
overlap = true;
} else {
for (int i = 0; i < count - 1; i++) {
if (buttons[i]->left - buttons[i]->gravityX + buttons[i]->width >= buttons[i + 1]->left - buttons[i + 1]->gravityX) {
overlap = true;
break;
}
}
}
//Shrink the font size if any buttons are overlap
if (overlap) {
for (int i = 0; i < count; i++) {
buttons[i]->smallFont = true;
buttons[i]->width = -1;
}
}
}
//Now we dim the screen and keep the GUI rendering/updating.
GUIOverlay* overlay=new GUIOverlay(renderer,root);
overlay->keyboardNavigationMode = LeftRightFocus | UpDownFocus | TabFocus | ((count == 1) ? 0 : ReturnControls);
overlay->enterLoop(imageManager, renderer, true, count == 1);
//And return the result.
return (msgBoxResult)objHandler.ret;
}
// A helper function to read a character from utf8 string
// s: the string
// p [in,out]: the position
// return value: the character readed, in utf32 format, 0 means end of string, -1 means error
int utf8ReadForward(const char* s, int& p) {
int ch = (unsigned char)s[p];
if (ch < 0x80){
if (ch) p++;
return ch;
} else if (ch < 0xC0){
// skip invalid characters
while (((unsigned char)s[p] & 0xC0) == 0x80) p++;
return -1;
} else if (ch < 0xE0){
int c2 = (unsigned char)s[++p];
if ((c2 & 0xC0) != 0x80) return -1;
ch = ((ch & 0x1F) << 6) | (c2 & 0x3F);
p++;
return ch;
} else if (ch < 0xF0){
int c2 = (unsigned char)s[++p];
if ((c2 & 0xC0) != 0x80) return -1;
int c3 = (unsigned char)s[++p];
if ((c3 & 0xC0) != 0x80) return -1;
ch = ((ch & 0xF) << 12) | ((c2 & 0x3F) << 6) | (c3 & 0x3F);
p++;
return ch;
} else if (ch < 0xF8){
int c2 = (unsigned char)s[++p];
if ((c2 & 0xC0) != 0x80) return -1;
int c3 = (unsigned char)s[++p];
if ((c3 & 0xC0) != 0x80) return -1;
int c4 = (unsigned char)s[++p];
if ((c4 & 0xC0) != 0x80) return -1;
ch = ((ch & 0x7) << 18) | ((c2 & 0x3F) << 12) | ((c3 & 0x3F) << 6) | (c4 & 0x3F);
if (ch >= 0x110000) ch = -1;
p++;
return ch;
} else {
p++;
return -1;
}
}
// A helper function to read a character backward from utf8 string (experimental)
// s: the string
// p [in,out]: the position
// return value: the character readed, in utf32 format, 0 means end of string, -1 means error
int utf8ReadBackward(const char* s, int& p) {
if (p <= 0) return 0;
do {
p--;
} while (p > 0 && ((unsigned char)s[p] & 0xC0) == 0x80);
int tmp = p;
return utf8ReadForward(s, tmp);
}
#ifndef WIN32
// ad-hoc function to check if a program is installed
static bool programExists(const std::string& program) {
std::string p = tfm::format("which \"%s\" 2>&1", program);
const int BUFSIZE = 128;
char buf[BUFSIZE];
FILE *fp;
if ((fp = popen(p.c_str(), "r")) == NULL) {
return false;
}
while (fgets(buf, BUFSIZE, fp) != NULL) {
// Drop all outputs since 'which' returns -1 when the program is not found
}
if (pclose(fp)) {
return false;
}
return true;
}
#endif
void openWebsite(const std::string& url) {
#ifdef WIN32
wchar_t ws[4096];
TO_UTF16(url.c_str(), ws);
SDL_SysWMinfo info = {};
SDL_VERSION(&info.version);
SDL_GetWindowWMInfo(sdlWindow, &info);
ShellExecuteW(info.info.win.window, L"open", ws, NULL, NULL, SW_SHOW);
#else
static int method = -1;
// Some of these methods are copied from https://stackoverflow.com/questions/5116473/
const char* methods[] = {
"xdg-open", "xdg-open \"%s\"",
"gnome-open", "gnome-open \"%s\"",
"kde-open", "kde-open \"%s\"",
"open", "open \"%s\"",
"python", "python -m webbrowser \"%s\"",
"sensible-browser", "sensible-browser \"%s\"",
"x-www-browser", "x-www-browser \"%s\"",
NULL,
};
if (method < 0) {
for (method = 0; methods[method]; method += 2) {
if (programExists(methods[method])) break;
}
}
if (methods[method]) {
std::string p = tfm::format(methods[method + 1], url);
system(p.c_str());
} else {
fprintf(stderr, "TODO: openWebsite is not implemented on your system\n");
}
#endif
}
std::string appendURLToLicense(const std::string& license) {
// if the license doesn't include url, try to detect it from a predefined list
if (license.find("://") == std::string::npos) {
std::string normalized;
for (char c : license) {
if ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z')) {
normalized.push_back(c);
} else if (c >= 'a' && c <= 'z') {
normalized.push_back(c + ('A' - 'a'));
}
}
const char* licenses[] = {
// AGPL
"AGPL1", "AGPLV1", NULL, "http://www.affero.org/oagpl.html",
"AGPL2", "AGPLV2", NULL, "http://www.affero.org/agpl2.html",
"AGPL", NULL, "https://gnu.org/licenses/agpl.html",
// LGPL
"LGPL21", "LGPLV21", NULL, "https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html",
"LGPL2", "LGPLV2", NULL, "https://www.gnu.org/licenses/old-licenses/lgpl-2.0.html",
"LGPL", NULL, "https://www.gnu.org/copyleft/lesser.html",
// GPL
"GPL1", "GPLV1", NULL, "https://www.gnu.org/licenses/old-licenses/gpl-1.0.html",
"GPL2", "GPLV2", NULL, "https://www.gnu.org/licenses/old-licenses/gpl-2.0.html",
"GPL", NULL, "https://gnu.org/licenses/gpl.html",
// CC BY-NC-ND
"CCBYNCND1", "CCBYNDNC1", NULL, "https://creativecommons.org/licenses/by-nd-nc/1.0",
"CCBYNCND25", "CCBYNDNC25", NULL, "https://creativecommons.org/licenses/by-nc-nd/2.5",
"CCBYNCND2", "CCBYNDNC2", NULL, "https://creativecommons.org/licenses/by-nc-nd/2.0",
"CCBYNCND3", "CCBYNDNC3", NULL, "https://creativecommons.org/licenses/by-nc-nd/3.0",
"CCBYNCND", "CCBYNDNC", NULL, "https://creativecommons.org/licenses/by-nc-nd/4.0",
// CC BY-NC-SA
"CCBYNCSA1", NULL, "https://creativecommons.org/licenses/by-nc-sa/1.0",
"CCBYNCSA25", NULL, "https://creativecommons.org/licenses/by-nc-sa/2.5",
"CCBYNCSA2", NULL, "https://creativecommons.org/licenses/by-nc-sa/2.0",
"CCBYNCSA3", NULL, "https://creativecommons.org/licenses/by-nc-sa/3.0",
"CCBYNCSA", NULL, "https://creativecommons.org/licenses/by-nc-sa/4.0",
// CC BY-ND
"CCBYND1", NULL, "https://creativecommons.org/licenses/by-nd/1.0",
"CCBYND25", NULL, "https://creativecommons.org/licenses/by-nd/2.5",
"CCBYND2", NULL, "https://creativecommons.org/licenses/by-nd/2.0",
"CCBYND3", NULL, "https://creativecommons.org/licenses/by-nd/3.0",
"CCBYND", NULL, "https://creativecommons.org/licenses/by-nd/4.0",
// CC BY-NC
"CCBYNC1", NULL, "https://creativecommons.org/licenses/by-nc/1.0",
"CCBYNC25", NULL, "https://creativecommons.org/licenses/by-nc/2.5",
"CCBYNC2", NULL, "https://creativecommons.org/licenses/by-nc/2.0",
"CCBYNC3", NULL, "https://creativecommons.org/licenses/by-nc/3.0",
"CCBYNC", NULL, "https://creativecommons.org/licenses/by-nc/4.0",
// CC BY-SA
"CCBYSA1", NULL, "https://creativecommons.org/licenses/by-sa/1.0",
"CCBYSA25", NULL, "https://creativecommons.org/licenses/by-sa/2.5",
"CCBYSA2", NULL, "https://creativecommons.org/licenses/by-sa/2.0",
"CCBYSA3", NULL, "https://creativecommons.org/licenses/by-sa/3.0",
"CCBYSA", NULL, "https://creativecommons.org/licenses/by-sa/4.0",
// CC BY
"CCBY1", NULL, "https://creativecommons.org/licenses/by/1.0",
"CCBY25", NULL, "https://creativecommons.org/licenses/by/2.5",
"CCBY2", NULL, "https://creativecommons.org/licenses/by/2.0",
"CCBY3", NULL, "https://creativecommons.org/licenses/by/3.0",
"CCBY", NULL, "https://creativecommons.org/licenses/by/4.0",
// CC0
"CC0", NULL, "https://creativecommons.org/publicdomain/zero/1.0",
// WTFPL
"WTFPL", NULL, "http://www.wtfpl.net/",
// end
NULL,
};
for (int i = 0; licenses[i]; i++) {
bool found = false;
for (; licenses[i]; i++) {
if (normalized.find(licenses[i]) != std::string::npos) found = true;
}
i++;
if (found) {
return license + tfm::format(" <%s>", licenses[i]);
}
}
}
return license;
}
int getKeyboardRepeatDelay() {
static int ret = -1;
if (ret < 0) {
#ifdef WIN32
int i = 0;
SystemParametersInfoW(SPI_GETKEYBOARDDELAY, 0, &i, 0);
// NOTE: these weird numbers are derived from Microsoft's documentation explaining the return value of SystemParametersInfo.
i = clamp(i, 0, 3);
ret = (i + 1) * 10;
#else
// TODO: platform-dependent code
// Assume it's 250ms, i.e. 10 frames
ret = 10;
#endif
// Debug
#ifdef _DEBUG
printf("getKeyboardRepeatDelay() = %d\n", ret);
#endif
}
return ret;
}
int getKeyboardRepeatInterval() {
static int ret = -1;
if (ret < 0) {
#ifdef WIN32
int i = 0;
SystemParametersInfoW(SPI_GETKEYBOARDSPEED, 0, &i, 0);
// NOTE: these weird numbers are derived from Microsoft's documentation explaining the return value of SystemParametersInfo.
i = clamp(i, 0, 31);
ret = (int)floor(40.0f / (2.5f + 0.887097f * (float)i) + 0.5f);
#else
// TODO: platform-dependent code
// Assume it's 25ms, i.e. 1 frame
ret = 1;
#endif
// Debug
#ifdef _DEBUG
printf("getKeyboardRepeatInterval() = %d\n", ret);
#endif
}
return ret;
}
diff --git a/src/Functions.h b/src/Functions.h
index 04fd988..d86784b 100644
--- a/src/Functions.h
+++ b/src/Functions.h
@@ -1,300 +1,294 @@
/*
* Copyright (C) 2011-2013 Me and My Shadow
*
* This file is part of Me and My Shadow.
*
* Me and My Shadow is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Me and My Shadow is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Me and My Shadow. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef FUNCTIONS_H
#define FUNCTIONS_H
#include "Settings.h"
#include "Globals.h"
#include <SDL.h>
#include <string>
#include <vector>
class MusicManager;
class SoundManager;
//gettext function
//message: The message to translate.
//Returns: The translated string or the original string if there is not translation available.
#define _(message) (dictionaryManager!=NULL?dictionaryManager->get_dictionary().translate(message).c_str():std::string(message).c_str())
//gettext function
//NOTE: "_C" is conflict to some Android macros so we change its name.
//dictionaryManager: Pointer to the dictionaryManager to use for the translation.
//message: The message to translate.
//Returns: The translated string or the original string if there is not translation available.
#define _CC(dictionaryManager, message) ((dictionaryManager)!=NULL?(dictionaryManager)->get_dictionary().translate(message).c_str():std::string(message).c_str())
//dummy function for xgettext
//message: The message to translate.
//Returns: message parameter
#define __(message) (message)
class ImageManager;
struct SDL_Texture;
class LevelPackManager;
//gettext function for plural forms
//message: The singular version of the message to translate.
//messageplural: The plural version of the message to translate.
//num: The number to fetch the plural form for
//Returns: The translated string or the original string if there are no translations available.
std::string ngettext(const std::string& message, const std::string& messageplural, int num);
//Method for drawing an SDL_Surface onto another.
//x: The x location to draw the source on the desination.
//y: The y location to draw the source on the desination.
//source: The SDL_Surface to draw.
//dest: The SDL_Surface to draw on.
//clip: Rectangle which part of the source should be drawn.
void applySurface(int x,int y,SDL_Surface* source,SDL_Surface* dest,SDL_Rect* clip);
//Method used to draw an rectangle.
//x: The top left x location of the rectangle.
//y: The top left y location of the rectangle.
//w: The width of the rectangle,
//h: The height of the rectangle.
//dest: The SDL_Surface to draw on.
//color: The color of the rectangle border to draw.
void drawRect(int x, int y, int w, int h, SDL_Renderer &renderer, Uint32 color=0);
//Method used to draw filled boxes with an anti-alliased border.
//Mostly used for GUI components.
//x: The top left x location of the box.
//y: The top left y location of the box.
//w: The width of the box,
//h: The height of the box.
//renderer: The SDL_Renderer to render on..
//color: The color of the rectangle background to draw.
void drawGUIBox(int x,int y,int w,int h,SDL_Renderer& renderer,Uint32 color);
//Method used to draw a line.
//x1: The x location of the start point.
//y1: The y location of the start point.
//x2: The x location of the end point.
//y2: The y location of the end point.
//dest: The SDL_Surface to draw on.
//color: The color of the line to draw.
void drawLine(int x1,int y1,int x2,int y2,SDL_Renderer &renderer,Uint32 color=0);
//Method used to draw a line with some arrows on it.
//x1: The x location of the start point.
//y1: The y location of the start point.
//x2: The x location of the end point.
//y2: The y location of the end point.
//dest: The SDL_Surface to draw on.
//color: The color of the line to draw.
//spacing: The spacing between arrows.
//offset: Offset of first arrow relative to the start point.
//xize, ysize: The size of arrow.
void drawLineWithArrow(int x1, int y1, int x2, int y2, SDL_Renderer &renderer, Uint32 color=0, int spacing=16, int offset=0, int xsize=5, int ysize=5);
//Method that will load the fonts needed for the game.
//NOTE: It's separate from loadFiles(), since it might get called separatly from the code when changing the language.
bool loadFonts();
//Method that will load the default theme again.
//name: name of the theme to load or empty for scaling background
//NOTE: It's separate from loadFiles(), since it might get called separatly from the code when changing resolution.
bool loadTheme(ImageManager& imageManager, SDL_Renderer& renderer, std::string name);
struct ScreenData{
SDL_Renderer* renderer;
explicit operator bool() const {
return renderer!=nullptr;
}
};
//This method will attempt to create the screen/window.
//NOTE: It's separate from init(), since it might get called separatly from the code when changing resolution.
ScreenData createScreen();
-
-//A very simple structure for resolutions.
-struct _res{
- int w,h;
-};
-
//Method for retrieving a list of resolutions.
-std::vector<_res> getResolutionList();
+std::vector<SDL_Point> getResolutionList();
//Method that is called when a fullscreen window is created.
//It will choose the resolution that is closest to the configured one.
void pickFullscreenResolution();
//This method is used to configure the window that is created by createScreen.
//NOTE: It will do it in a WM specific way, so if the wm is unkown it will do nothing.
void configureWindow();
//Call this method when receive SDL_VIDEORESIZE event.
void onVideoResize(ImageManager &imageManager, SDL_Renderer& renderer);
//Initialises the game. This is done almost at the beginning of the program.
//It initialises: SDL, SDL_Mixer, SDL_ttf, the screen and the block types.
//Returns: True if everything goes well.
ScreenData init();
//Loads some important files, like the background music and the default theme.
//Returns: True if everything goes well.
bool loadFiles(ImageManager &imageManager, SDL_Renderer &renderer);
//This method will load the settings from the settings file.
//Returns: False if there's an error while loading.
bool loadSettings();
//This method will save the settings to the settings file.
//Returns: False if there's an error while saving.
bool saveSettings();
//Method used to get a pointer to the settings object.
//Returns: A pointer to the settings object.
Settings* getSettings();
//Method used to get a pointer to the MusicManager object.
//Returns: A pointer to the MusicManager object.
MusicManager* getMusicManager();
//Method used to get a pointer to the SoundManager object.
//Returns: A pointer to the SoundManager object.
SoundManager* getSoundManager();
//Method used to get a pointer to the LevelPackManager object.
//Returns: A pointer to the LevelPackManager object.
LevelPackManager* getLevelPackManager();
//Method that will, depending on the rendering backend, draw the screen surface to the screen.
void flipScreen(SDL_Renderer& renderer);
//Method used to clean up before quiting meandmyshadow.
void clean();
//Sets what the nextState will be.
//newstate: Integer containing the id of the newstate.
void setNextState(int newstate);
//Method that will perform the state change.
//It will fade out (but not fade in).
void changeState(ImageManager &imageManager, SDL_Renderer &renderer, int fade = 255);
//This method is called when music is stopped.
//NOTE: This method is outside the MusicManager because it can't be called otherwise by SDL_Mixer.
//Do not call this method anywhere in the code!
void musicStoppedHook();
//This method is called when a sfx finished playing.
//NOTE: This method is outside the SoundManager because it can't be called otherwise by SDL_Mixer.
//Do not call this method anywhere in the code!
//channel: The number of the channel that is finished.
void channelFinishedHook(int channel);
//Checks collision between two SDL_Rects.
//a: The first rectangle.
//b: The second rectangle.
//Returns: True if the two rectangles collide.
bool checkCollision(const SDL_Rect& a,const SDL_Rect& b);
//Checks if a given point lays on an SDL_Rect
//point: The point to check.
//rect: The rectangle to check.
//Returns: True if the point is on the rectangle.
bool pointOnRect(const SDL_Rect& a,const SDL_Rect& b);
//Parse the commandline arguments.
//argc: Integer containing the number of aruguments there are.
//argv: The arguments.
//Returns: -1 if something goes wrong while parsing,
// 0 if version is shown,
// 1 if everything is alright
int parseArguments(int argc, char** argv);
//From http://en.wikipedia.org/wiki/Clamping_(graphics)
//x: The value to clamp.
//min: The minimum x can be.
//max: The maximum x can be.
//Returns: Integer containing the clamped value.
int inline clamp(int x,int min,int max){
return (x>max)?max:(x<min)?min:x;
}
//Enumeration containing the different messagebox button combinations.
enum msgBoxButtons{
//Only one button with the text OK.
MsgBoxOKOnly=0,
//Two buttons, one saying OK, the other Cancel.
MsgBoxOKCancel=1,
//Three buttons, Abort, Retry, Ignore.
MsgBoxAbortRetryIgnore=2,
//Three buttons, Yes, No or Cancel.
MsgBoxYesNoCancel=3,
//Two buttons, one saying Yes, the other No.
MsgBoxYesNo=4,
//Two buttons, one saying Retry, the other Cancel.
MsgBoxRetryCancel=5,
};
//Enumeration containing the different result that can be retrieved from a messagebox.
//It represents the button that has been pressed.
enum msgBoxResult{
//The OK button.
MsgBoxOK=1,
//The cancel button.
MsgBoxCancel=2,
//The abort button.
MsgBoxAbort=3,
//The retry button.
MsgBoxRetry=4,
//The ignore button.
MsgBoxIgnore=5,
//The yes button.
MsgBoxYes=6,
//The no button.
MsgBoxNo=7,
};
//Method that prompts the user with a notification and/or question.
//prompt: The message the user is prompted with.
//buttons: Which buttons the messagebox should have.
//title: The title of the message box.
//Returns: A msgBoxResult which button has been pressed.
msgBoxResult msgBox(ImageManager& imageManager,SDL_Renderer& renderer, const std::string& prompt,msgBoxButtons buttons,const std::string& title);
// A helper function to read a character from utf8 string
// s: the string
// p [in,out]: the position
// return value: the character readed, in utf32 format, 0 means end of string, -1 means error
int utf8ReadForward(const char* s, int& p);
// A helper function to read a character backward from utf8 string (experimental)
// s: the string
// p [in,out]: the position
// return value: the character readed, in utf32 format, 0 means end of string, -1 means error
int utf8ReadBackward(const char* s, int& p);
// Open the website using default web browser.
// url: The url of the website. Currently only http and https are supported.
// Also we assume that the url only contains ASCII characters.
// WARNING: Passing other url may result in arbitrary behavior (esp. passing '*.exe' on Windows).
void openWebsite(const std::string& url);
// Append a URL to a license if the license doesn't include URL (try to detect it from a predefined list).
// license: The license.
// Return value: The license appended with a URL if we detect the license successfully.
std::string appendURLToLicense(const std::string& license);
// Retrieves the (approximate) keyboard repeat delay time, in frames (NOTE: frame rate is hardcoded as 40).
int getKeyboardRepeatDelay();
// Retrieves the (approximate) keyboard repeat interval time, in frames (NOTE: frame rate is hardcoded as 40).
int getKeyboardRepeatInterval();
#endif
diff --git a/src/Game.cpp b/src/Game.cpp
index 193ca46..5584086 100644
--- a/src/Game.cpp
+++ b/src/Game.cpp
@@ -1,1840 +1,1835 @@
/*
* Copyright (C) 2011-2013 Me and My Shadow
*
* This file is part of Me and My Shadow.
*
* Me and My Shadow is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Me and My Shadow is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Me and My Shadow. If not, see <http://www.gnu.org/licenses/>.
*/
#include "Block.h"
#include "GameState.h"
#include "Functions.h"
#include "GameObjects.h"
#include "ThemeManager.h"
#include "Game.h"
#include "TreeStorageNode.h"
#include "POASerializer.h"
#include "InputManager.h"
#include "MusicManager.h"
#include "Render.h"
#include "StatisticsManager.h"
#include "ScriptExecutor.h"
#include <fstream>
#include <iostream>
#include <sstream>
#include <vector>
#include <map>
#include <algorithm>
#include <stdio.h>
#include <SDL_ttf.h>
#include "libs/tinyformat/tinyformat.h"
using namespace std;
const char* Game::blockName[TYPE_MAX]={"Block","PlayerStart","ShadowStart",
"Exit","ShadowBlock","Spikes",
"Checkpoint","Swap","Fragile",
"MovingBlock","MovingShadowBlock","MovingSpikes",
"Teleporter","Button","Switch",
"ConveyorBelt","ShadowConveyorBelt","NotificationBlock", "Collectable", "Pushable"
};
map<string,int> Game::blockNameMap;
map<int,string> Game::gameObjectEventTypeMap;
map<string,int> Game::gameObjectEventNameMap;
map<int,string> Game::levelEventTypeMap;
map<string,int> Game::levelEventNameMap;
string Game::recordFile;
//An internal function.
static void copyCompiledScripts(lua_State *state, const std::map<int, int>& src, std::map<int, int>& dest) {
//Clear the existing scripts.
for (auto it = dest.begin(); it != dest.end(); ++it) {
luaL_unref(state, LUA_REGISTRYINDEX, it->second);
}
dest.clear();
//Copy the source to the destination.
for (auto it = src.begin(); it != src.end(); ++it) {
lua_rawgeti(state, LUA_REGISTRYINDEX, it->second);
dest[it->first] = luaL_ref(state, LUA_REGISTRYINDEX);
}
}
Game::Game(SDL_Renderer &renderer, ImageManager &imageManager):isReset(false)
, scriptExecutor(new ScriptExecutor())
,currentLevelNode(NULL)
,customTheme(NULL)
,background(NULL)
,won(false)
,interlevel(false)
,gameTipIndex(0)
,time(0),timeSaved(0)
,recordings(0),recordingsSaved(0)
,cameraMode(CAMERA_PLAYER),cameraModeSaved(CAMERA_PLAYER)
,player(this),shadow(this),objLastCheckPoint(NULL)
,currentCollectables(0),totalCollectables(0),currentCollectablesSaved(0){
saveStateNextTime=false;
loadStateNextTime=false;
recentSwap=recentSwapSaved=-10000;
recentLoad=recentSave=0;
action=imageManager.loadTexture(getDataPath()+"gfx/actions.png", renderer);
medals=imageManager.loadTexture(getDataPath()+"gfx/medals.png", renderer);
//Get the collectable image from the theme.
//NOTE: Isn't there a better way to retrieve the image?
objThemes.getBlock(TYPE_COLLECTABLE)->createInstance(&collectable);
//Hide the cursor if not in the leveleditor.
if(stateID!=STATE_LEVEL_EDITOR)
SDL_ShowCursor(SDL_DISABLE);
}
Game::~Game(){
//Simply call our destroy method.
destroy();
//Before we leave make sure the cursor is visible.
SDL_ShowCursor(SDL_ENABLE);
}
void Game::destroy(){
delete scriptExecutor;
scriptExecutor = NULL;
//Loop through the levelObjects and delete them.
for(unsigned int i=0;i<levelObjects.size();i++)
delete levelObjects[i];
//Done now clear the levelObjects vector.
levelObjects.clear();
//Loop through the sceneryLayers and delete them.
for(auto it=sceneryLayers.begin();it!=sceneryLayers.end();++it){
delete it->second;
}
sceneryLayers.clear();
//Clear the name and the editor data.
levelName.clear();
levelFile.clear();
editorData.clear();
//Remove everything from the themeManager.
background=NULL;
if(customTheme)
objThemes.removeTheme();
customTheme=NULL;
//If there's a (partial) theme bundled with the levelpack remove that as well.
if(levels->customTheme)
objThemes.removeTheme();
//delete current level (if any)
if(currentLevelNode){
delete currentLevelNode;
currentLevelNode=NULL;
}
//Reset the time.
time=timeSaved=0;
recordings=recordingsSaved=0;
recentSwap=recentSwapSaved=-10000;
//Set the music list back to the configured list.
getMusicManager()->setMusicList(getSettings()->getValue("musiclist"));
}
void Game::reloadMusic() {
//NOTE: level music is always enabled.
//Check if the levelpack has a prefered music list.
if (levels && !levels->levelpackMusicList.empty())
getMusicManager()->setMusicList(levels->levelpackMusicList);
//Check for the music to use.
string &s = editorData["music"];
if (!s.empty()) {
getMusicManager()->playMusic(s);
} else {
getMusicManager()->pickMusic();
}
}
void Game::loadLevelFromNode(ImageManager& imageManager,SDL_Renderer& renderer,TreeStorageNode* obj,const string& fileName){
//Make sure there's nothing left from any previous levels.
//Not needed since loadLevelFromNode is only called from the changeState method, meaning it's a new instance of Game.
//destroy();
//set current level to loaded one.
currentLevelNode=obj;
//Set the level dimensions to the default, it will probably be changed by the editorData,
//but 800x600 is a fallback.
LEVEL_WIDTH=800;
LEVEL_HEIGHT=600;
currentCollectables=0;
totalCollectables=0;
currentCollectablesSaved=0;
//Load the additional data.
for(map<string,vector<string> >::iterator i=obj->attributes.begin();i!=obj->attributes.end();++i){
if(i->first=="size"){
//We found the size attribute.
if(i->second.size()>=2){
//Set the dimensions of the level.
LEVEL_WIDTH=atoi(i->second[0].c_str());
LEVEL_HEIGHT=atoi(i->second[1].c_str());
}
}else if(i->second.size()>0){
//Any other data will be put into the editorData.
editorData[i->first]=i->second[0];
}
}
//Get the theme.
{
//NOTE: level themes are always enabled.
//Check for bundled (partial) themes for level pack.
if (levels->customTheme){
if (objThemes.appendThemeFromFile(levels->levelpackPath + "/theme/theme.mnmstheme", imageManager, renderer) == NULL){
//The theme failed to load so set the customTheme boolean to false.
levels->customTheme = false;
}
}
//Check for the theme to use for this level. This has higher priority.
//Examples: %DATA%/themes/classic or %USER%/themes/Orange
string &s = editorData["theme"];
if (!s.empty()){
customTheme = objThemes.appendThemeFromFile(processFileName(s) + "/theme.mnmstheme", imageManager, renderer);
}
//Set the Appearance of the player and the shadow.
objThemes.getCharacter(false)->createInstance(&player.appearance);
objThemes.getCharacter(true)->createInstance(&shadow.appearance);
}
//Get the music.
reloadMusic();
//Load the data from the level node.
for(unsigned int i=0;i<obj->subNodes.size();i++){
TreeStorageNode* obj1=obj->subNodes[i];
if(obj1==NULL) continue;
if(obj1->name=="tile"){
Block* block=new Block(this);
if(!block->loadFromNode(imageManager,renderer,obj1)){
delete block;
continue;
}
//If the type is collectable, increase the number of totalCollectables
if(block->type==TYPE_COLLECTABLE)
totalCollectables++;
//Add the block to the levelObjects vector.
levelObjects.push_back(block);
}else if(obj1->name=="scenerylayer" && obj1->value.size()==1){
//Check if the layer exists.
if (sceneryLayers[obj1->value[0]] == NULL) {
sceneryLayers[obj1->value[0]] = new SceneryLayer();
}
//Load contents from node.
sceneryLayers[obj1->value[0]]->loadFromNode(this, imageManager, renderer, obj1);
}else if(obj1->name=="script" && !obj1->value.empty()){
map<string,int>::iterator it=Game::levelEventNameMap.find(obj1->value[0]);
if(it!=Game::levelEventNameMap.end()){
int eventType=it->second;
const std::string& script=obj1->attributes["script"][0];
if(!script.empty()) scripts[eventType]=script;
}
}
}
//Set the levelName to the name of the current level.
levelName=editorData["name"];
levelFile=fileName;
//Some extra stuff only needed when not in the levelEditor.
if(stateID!=STATE_LEVEL_EDITOR){
//We create a text with the text "Level <levelno> <levelName>".
//It will be shown in the left bottom corner of the screen.
string s;
if(levels->getLevelCount()>1 && levels->type!=COLLECTION){
s=tfm::format(_("Level %d %s"),levels->getCurrentLevel()+1,_CC(levels->getDictionaryManager(),editorData["name"]));
} else {
s = _CC(levels->getDictionaryManager(), editorData["name"]);
}
bmTips[0]=textureFromText(renderer, *fontText,s.c_str(),objThemes.getTextColor(true));
}
//Get the background
background=objThemes.getBackground(false);
//Now the loading is finished, we reset all objects to their initial states.
reset(true, stateID == STATE_LEVEL_EDITOR);
}
void Game::loadLevel(ImageManager& imageManager,SDL_Renderer& renderer,std::string fileName){
//Create a TreeStorageNode that will hold the loaded data.
TreeStorageNode *obj=new TreeStorageNode();
{
POASerializer objSerializer;
string s=fileName;
//Parse the file.
if(!objSerializer.loadNodeFromFile(s.c_str(),obj,true)){
cerr<<"ERROR: Can't load level file "<<s<<endl;
delete obj;
return;
}
}
//Now call another function.
loadLevelFromNode(imageManager,renderer,obj,fileName);
}
void Game::saveRecord(const char* fileName){
//check if current level is NULL (which should be impossible)
if(currentLevelNode==NULL) return;
TreeStorageNode obj;
POASerializer objSerializer;
//put current level to the node.
currentLevelNode->name="map";
obj.subNodes.push_back(currentLevelNode);
//serialize the game record using RLE compression.
#define PUSH_BACK \
if(j>0){ \
if(j>1){ \
sprintf(c,"%d*%d",last,j); \
}else{ \
sprintf(c,"%d",last); \
} \
v.push_back(c); \
}
vector<string> &v=obj.attributes["record"];
vector<int> *record=player.getRecord();
char c[64];
int i,j=0,last;
for(i=0;i<(int)record->size();i++){
int currentKey=(*record)[i];
if(j==0 || currentKey!=last){
PUSH_BACK;
last=currentKey;
j=1;
}else{
j++;
}
}
PUSH_BACK;
#undef PUSH_BACK
#ifdef RECORD_FILE_DEBUG
//add record file debug data.
{
obj.attributes["recordKeyPressLog"].push_back(player.keyPressLog());
vector<SDL_Rect> &playerPosition=player.playerPosition();
string s;
char c[32];
sprintf(c,"%d\n",int(playerPosition.size()));
s=c;
for(unsigned int i=0;i<playerPosition.size();i++){
SDL_Rect& r=playerPosition[i];
sprintf(c,"%d %d\n",r.x,r.y);
s+=c;
}
obj.attributes["recordPlayerPosition"].push_back(s);
}
#endif
//save it
objSerializer.saveNodeToFile(fileName,&obj,true,true);
//remove current level from node to prevent delete it.
obj.subNodes.clear();
}
void Game::loadRecord(ImageManager& imageManager, SDL_Renderer& renderer, const char* fileName){
//Create a TreeStorageNode that will hold the loaded data.
TreeStorageNode obj;
{
POASerializer objSerializer;
string s=fileName;
//Parse the file.
if(!objSerializer.loadNodeFromFile(s.c_str(),&obj,true)){
cerr<<"ERROR: Can't load record file "<<s<<endl;
return;
}
}
//find the node named 'map'.
bool loaded=false;
for(unsigned int i=0;i<obj.subNodes.size();i++){
if(obj.subNodes[i]->name=="map"){
//load the level. (fileName=???)
loadLevelFromNode(imageManager,renderer,obj.subNodes[i],"???");
//remove this node to prevent delete it.
obj.subNodes[i]=NULL;
//over
loaded=true;
break;
}
}
if(!loaded){
cerr<<"ERROR: Can't find subnode named 'map' from record file"<<endl;
return;
}
//load the record.
{
vector<int> *record=player.getRecord();
record->clear();
vector<string> &v=obj.attributes["record"];
for(unsigned int i=0;i<v.size();i++){
string &s=v[i];
string::size_type pos=s.find_first_of('*');
if(pos==string::npos){
//1 item only.
int i=atoi(s.c_str());
record->push_back(i);
}else{
//contains many items.
int i=atoi(s.substr(0,pos).c_str());
int j=atoi(s.substr(pos+1).c_str());
for(;j>0;j--){
record->push_back(i);
}
}
}
}
#ifdef RECORD_FILE_DEBUG
//load the debug data
{
vector<string> &v=obj.attributes["recordPlayerPosition"];
vector<SDL_Rect> &playerPosition=player.playerPosition();
playerPosition.clear();
if(!v.empty()){
if(!v[0].empty()){
stringstream st(v[0]);
int m;
st>>m;
for(int i=0;i<m;i++){
SDL_Rect r;
st>>r.x>>r.y;
r.w=0;
r.h=0;
playerPosition.push_back(r);
}
}
}
}
#endif
//play the record.
//TODO: tell the level manager don't save the level progress.
player.playRecord();
shadow.playRecord(); //???
}
/////////////EVENT///////////////
void Game::handleEvents(ImageManager& imageManager, SDL_Renderer& renderer){
//First of all let the player handle input.
player.handleInput(&shadow);
//Check for an SDL_QUIT event.
if(event.type==SDL_QUIT){
//We need to quit so enter STATE_EXIT.
setNextState(STATE_EXIT);
}
//Check for the escape key.
if(stateID != STATE_LEVEL_EDITOR && inputMgr.isKeyDownEvent(INPUTMGR_ESCAPE)){
//Escape means we go one level up, to the level select state.
setNextState(STATE_LEVEL_SELECT);
//Save the progress.
levels->saveLevelProgress();
//And change the music back to the menu music.
getMusicManager()->playMusic("menu");
}
//Check if 'R' is pressed.
if(inputMgr.isKeyDownEvent(INPUTMGR_RESTART)){
//Restart game only if we are not watching a replay.
if (!player.isPlayFromRecord() || interlevel) {
//Reset the game at next frame.
isReset = true;
//Also delete any gui (most likely the interlevel gui). Only in game mode.
if (GUIObjectRoot && stateID != STATE_LEVEL_EDITOR){
delete GUIObjectRoot;
GUIObjectRoot = NULL;
}
//And set interlevel to false.
interlevel = false;
}
}
//Check for the next level buttons when in the interlevel popup.
if (inputMgr.isKeyDownEvent(INPUTMGR_SPACE) || inputMgr.isKeyDownEvent(INPUTMGR_SELECT)){
if(interlevel){
//The interlevel popup is shown so we need to delete it.
if(GUIObjectRoot){
delete GUIObjectRoot;
GUIObjectRoot=NULL;
}
//Now goto the next level.
gotoNextLevel(imageManager,renderer);
}
}
//Check if tab is pressed.
if(inputMgr.isKeyDownEvent(INPUTMGR_TAB)){
//Switch the camera mode.
switch(cameraMode){
case CAMERA_PLAYER:
cameraMode=CAMERA_SHADOW;
break;
case CAMERA_SHADOW:
case CAMERA_CUSTOM:
cameraMode=CAMERA_PLAYER;
break;
}
}
}
/////////////////LOGIC///////////////////
void Game::logic(ImageManager& imageManager, SDL_Renderer& renderer){
//Add one tick to the time.
time++;
//NOTE: This code reverts some changes in commit 5f03ae5.
//This is part of old prepareFrame() code.
//This is needed since otherwise the script function block:setLocation() and block:moveTo() are completely broken.
//Later we should rewrite collision system completely which will remove this piece of code.
//NOTE: In new collision system the effect of dx/dy/xVel/yVel should only be restricted in one frame.
for (auto obj : levelObjects) {
switch (obj->type) {
default:
obj->dx = obj->dy = obj->xVel = obj->yVel = 0;
break;
case TYPE_PUSHABLE:
//NOTE: Currently the dx/dy/etc. of pushable blocks are still carry across frames, in order to make the collision system work correct.
break;
case TYPE_CONVEYOR_BELT: case TYPE_SHADOW_CONVEYOR_BELT:
//NOTE: We let the conveyor belt itself to reset its xVel/yVel.
obj->dx = obj->dy = 0;
break;
}
}
//NOTE2: The above code breaks pushable block with moving block in most cases,
//more precisely, if the pushable block is processed before the moving block then things may be broken.
//Process delay execution scripts.
getScriptExecutor()->processDelayExecution();
//Process any event in the queue.
for(unsigned int idx=0;idx<eventQueue.size();idx++){
//Get the event from the queue.
typeGameObjectEvent &e=eventQueue[idx];
//Check if the it has an id attached to it.
if(e.target){
//NOTE: Should we check if the target still exists???
e.target->onEvent(e.eventType);
}else if(e.flags&1){
//Loop through the levelObjects and give them the event if they have the right id.
for(unsigned int i=0;i<levelObjects.size();i++){
if(e.objectType<0 || levelObjects[i]->type==e.objectType){
if(levelObjects[i]->id==e.id){
levelObjects[i]->onEvent(e.eventType);
}
}
}
}else{
//Loop through the levelObjects and give them the event.
for(unsigned int i=0;i<levelObjects.size();i++){
if(e.objectType<0 || levelObjects[i]->type==e.objectType){
levelObjects[i]->onEvent(e.eventType);
}
}
}
}
//Done processing the events so clear the queue.
eventQueue.clear();
//Check if we should save/load state.
//NOTE: This happens after event handling so no eventQueue has to be saved/restored.
if(saveStateNextTime){
saveState();
}else if(loadStateNextTime){
loadState();
}
saveStateNextTime=false;
loadStateNextTime=false;
//Loop through the gameobjects to update them.
for(unsigned int i=0;i<levelObjects.size();i++){
//Send GameObjectEvent_OnEnterFrame event to the script
levelObjects[i]->onEvent(GameObjectEvent_OnEnterFrame);
}
//Let the gameobject handle movement.
{
std::vector<Block*> pushableBlocks;
//First we process blocks which are not pushable blocks.
for (auto o : levelObjects) {
if (o->type == TYPE_PUSHABLE) {
pushableBlocks.push_back(o);
} else {
o->move();
}
}
//Sort pushable blocks by their position, which is an ad-hoc workaround for
//<https://forum.freegamedev.net/viewtopic.php?f=48&t=8047#p77692>.
std::stable_sort(pushableBlocks.begin(), pushableBlocks.end(),
[](const Block* obj1, const Block* obj2)->bool
{
SDL_Rect r1 = const_cast<Block*>(obj1)->getBox(), r2 = const_cast<Block*>(obj2)->getBox();
if (r1.y > r2.y) return true;
else if (r1.y < r2.y) return false;
else return r1.x < r2.x;
});
//Now we process pushable blocks.
for (auto o : pushableBlocks) {
o->move();
}
}
//Also update the scenery.
for (auto it = sceneryLayers.begin(); it != sceneryLayers.end(); ++it){
it->second->updateAnimation();
}
//Let the player store his move, if recording.
player.shadowSetState();
//Let the player give his recording to the shadow, if configured.
player.shadowGiveState(&shadow);
//NOTE: to fix bugs regarding player/shadow swap, we should first process collision of player/shadow then move them
const SDL_Rect playerLastPosition = player.getBox();
const SDL_Rect shadowLastPosition = shadow.getBox();
//NOTE: The following is ad-hoc code to fix shadow on blocked player on conveyor belt bug
if (shadow.holdingOther) {
//We need to process shadow collision first if shadow is holding player.
//Let the shadow decide his move, if he's playing a recording.
shadow.moveLogic();
//Check collision for shadow.
shadow.collision(levelObjects, NULL);
//Get the new position of it.
const SDL_Rect r = shadow.getBox();
//Check collision for player. Transfer the velocity of shadow to it only if the shadow moves its position.
player.collision(levelObjects, (r.x != shadowLastPosition.x || r.y != shadowLastPosition.y) ? &shadow : NULL);
} else {
//Otherwise we process player first.
//Check collision for player.
player.collision(levelObjects, NULL);
//Get the new position of it.
const SDL_Rect r = player.getBox();
//Now let the shadow decide his move, if he's playing a recording.
shadow.moveLogic();
//Check collision for shadow. Transfer the velocity of player to it only if the player moves its position.
shadow.collision(levelObjects, (r.x != playerLastPosition.x || r.y != playerLastPosition.y) ? &player : NULL);
}
//Let the player move.
player.move(levelObjects, playerLastPosition.x, playerLastPosition.y);
//Let the shadow move.
shadow.move(levelObjects, shadowLastPosition.x, shadowLastPosition.y);
//Check collision and stuff for the shadow and player.
player.otherCheck(&shadow);
//Update the camera.
switch(cameraMode){
case CAMERA_PLAYER:
player.setMyCamera();
break;
case CAMERA_SHADOW:
shadow.setMyCamera();
break;
case CAMERA_CUSTOM:
//NOTE: The target is (should be) screen size independent so calculate the real target x and y here.
int targetX=cameraTarget.x-(SCREEN_WIDTH/2);
int targetY=cameraTarget.y-(SCREEN_HEIGHT/2);
//Move the camera to the cameraTarget.
if(camera.x>targetX){
camera.x-=(camera.x-targetX)>>4;
//Make sure we don't go too far.
if(camera.x<targetX)
camera.x=targetX;
}else if(camera.x<targetX){
camera.x+=(targetX-camera.x)>>4;
//Make sure we don't go too far.
if(camera.x>targetX)
camera.x=targetX;
}
if(camera.y>targetY){
camera.y-=(camera.y-targetY)>>4;
//Make sure we don't go too far.
if(camera.y<targetY)
camera.y=targetY;
}else if(camera.y<targetY){
camera.y+=(targetY-camera.y)>>4;
//Make sure we don't go too far.
if(camera.y>targetY)
camera.y=targetY;
}
break;
}
//Check if we won.
if(won){
//Check if it's playing from record
if(player.isPlayFromRecord() && !interlevel){
recordingEnded(imageManager,renderer);
}else{
//the string to store auto-save record path.
string bestTimeFilePath,bestRecordingFilePath;
//and if we can't get test path.
bool filePathError=false;
//Get current level
LevelPack::Level *level=levels->getLevel();
//Now check if we should update statistics
{
//Get previous and current medal
int oldMedal=level->won?1:0,newMedal=1;
int bestTime=level->time;
int targetTime=level->targetTime;
int bestRecordings=level->recordings;
int targetRecordings=level->targetRecordings;
if(oldMedal){
if(bestTime>=0 && (targetTime<0 || bestTime<=targetTime))
oldMedal++;
if(bestRecordings>=0 && (targetRecordings<0 || bestRecordings<=targetRecordings))
oldMedal++;
}else{
bestTime=time;
bestRecordings=recordings;
}
if(bestTime<0 || bestTime>time) bestTime=time;
if(bestRecordings<0 || bestRecordings>recordings) bestRecordings=recordings;
if(targetTime<0 || bestTime<=targetTime)
newMedal++;
if(targetRecordings<0 || bestRecordings<=targetRecordings)
newMedal++;
//Check if we need to update statistics
if(newMedal>oldMedal){
switch(oldMedal){
case 0:
statsMgr.completedLevels++;
break;
case 2:
statsMgr.silverLevels--;
break;
}
switch(newMedal){
case 2:
statsMgr.silverLevels++;
break;
case 3:
statsMgr.goldLevels++;
break;
}
}
}
//Check the achievement "Complete a level with checkpoint, but without saving"
if (objLastCheckPoint == NULL) {
for (auto obj : levelObjects) {
if (obj->type == TYPE_CHECKPOINT) {
statsMgr.newAchievement("withoutsave");
break;
}
}
}
//Set the current level won.
level->won=true;
if(level->time==-1 || level->time>time){
level->time=time;
//save the best-time game record.
if(bestTimeFilePath.empty()){
getCurrentLevelAutoSaveRecordPath(bestTimeFilePath,bestRecordingFilePath,true);
}
if(bestTimeFilePath.empty()){
cerr<<"ERROR: Couldn't get auto-save record file path"<<endl;
filePathError=true;
}else{
saveRecord(bestTimeFilePath.c_str());
}
}
if(level->recordings==-1 || level->recordings>recordings){
level->recordings=recordings;
//save the best-recordings game record.
if(bestRecordingFilePath.empty() && !filePathError){
getCurrentLevelAutoSaveRecordPath(bestTimeFilePath,bestRecordingFilePath,true);
}
if(bestRecordingFilePath.empty()){
cerr<<"ERROR: Couldn't get auto-save record file path"<<endl;
filePathError=true;
}else{
saveRecord(bestRecordingFilePath.c_str());
}
}
//Set the next level unlocked if it exists.
if(levels->getCurrentLevel()+1<levels->getLevelCount()){
levels->setLocked(levels->getCurrentLevel()+1);
}
//And save the progress.
levels->saveLevelProgress();
//Now go to the interlevel screen.
replayPlay(imageManager,renderer);
//Update achievements
if(levels->levelpackName=="tutorial") statsMgr.updateTutorialAchievements();
statsMgr.updateLevelAchievements();
//NOTE: We set isReset false to prevent the user from getting a best time of 0.00s and 0 recordings.
isReset = false;
}
}
won=false;
//Check if we should reset.
if (isReset) {
//NOTE: we don't need to reset save ??? it looks like that there are no bugs
reset(false, false);
}
isReset=false;
}
/////////////////RENDER//////////////////
void Game::render(ImageManager&,SDL_Renderer &renderer){
//First of all render the background.
{
//Get a pointer to the background.
ThemeBackground* bg=background;
//Check if the background is null, but there are themes.
if(bg==NULL && objThemes.themeCount()>0){
//Get the background from the first theme in the stack.
bg=objThemes[0]->getBackground(false);
}
//Check if the background isn't null.
if(bg){
//It isn't so draw it.
bg->draw(renderer);
//And if it's the loaded background then also update the animation.
//FIXME: Updating the animation in the render method?
if(bg==background)
bg->updateAnimation();
}else{
//There's no background so fill the screen with white.
SDL_SetRenderDrawColor(&renderer, 255,255,255,255);
SDL_RenderClear(&renderer);
}
}
//Now draw the blackground layers.
auto it = sceneryLayers.begin();
for (; it != sceneryLayers.end(); ++it){
if (it->first >= "f") break; // now we meet a foreground layer
it->second->show(renderer);
}
//Now we draw the levelObjects.
{
//NEW: always render the pushable blocks in front of other blocks
std::vector<Block*> pushableBlocks;
for (auto o : levelObjects) {
if (o->type == TYPE_PUSHABLE) {
pushableBlocks.push_back(o);
} else {
o->show(renderer);
}
}
for (auto o : pushableBlocks) {
o->show(renderer);
}
}
//Followed by the player and the shadow.
//NOTE: We draw the shadow first, because he needs to be behind the player.
shadow.show(renderer);
player.show(renderer);
//Now draw the foreground layers.
for (; it != sceneryLayers.end(); ++it){
it->second->show(renderer);
}
//Show the levelName if it isn't the level editor.
if(stateID!=STATE_LEVEL_EDITOR && bmTips[0]!=NULL && !interlevel){
withTexture(*bmTips[0], [&](SDL_Rect r){
drawGUIBox(-2,SCREEN_HEIGHT-r.h-4,r.w+8,r.h+6,renderer,0xFFFFFFFF);
applyTexture(2,SCREEN_HEIGHT-r.h,*bmTips[0],renderer,NULL);
});
}
//Check if there's a tooltip.
//NOTE: gameTipIndex 0 is used for the levelName, 1 for shadow death, 2 for restart text, 3 for restart+checkpoint.
if(gameTipIndex>3 && gameTipIndex<TYPE_MAX){
//Check if there's a tooltip for the type.
if(bmTips[gameTipIndex]==NULL){
//There isn't thus make it.
string s;
string keyCode = InputManagerKeyCode::describeTwo(inputMgr.getKeyCode(INPUTMGR_ACTION, false), inputMgr.getKeyCode(INPUTMGR_ACTION, true));
transform(keyCode.begin(),keyCode.end(),keyCode.begin(),::toupper);
switch(gameTipIndex){
case TYPE_CHECKPOINT:
/// TRANSLATORS: Please do not remove %s from your translation:
/// - %s will be replaced with current action key
s=tfm::format(_("Press %s key to save the game."),keyCode);
break;
case TYPE_SWAP:
/// TRANSLATORS: Please do not remove %s from your translation:
/// - %s will be replaced with current action key
s=tfm::format(_("Press %s key to swap the position of player and shadow."),keyCode);
break;
case TYPE_SWITCH:
/// TRANSLATORS: Please do not remove %s from your translation:
/// - %s will be replaced with current action key
s=tfm::format(_("Press %s key to activate the switch."),keyCode);
break;
case TYPE_PORTAL:
/// TRANSLATORS: Please do not remove %s from your translation:
/// - %s will be replaced with current action key
s=tfm::format(_("Press %s key to teleport."),keyCode);
break;
}
//If we have a string then it's a supported GameObject type.
if(!s.empty()){
bmTips[gameTipIndex]=textureFromText(renderer, *fontText, s.c_str(), objThemes.getTextColor(true));
}
}
//We already have a gameTip for this type so draw it.
if(bmTips[gameTipIndex]!=NULL){
withTexture(*bmTips[gameTipIndex], [&](SDL_Rect r){
drawGUIBox(-2,-2,r.w+8,r.h+6,renderer,0xFFFFFFFF);
applyTexture(2,2,*bmTips[gameTipIndex],renderer);
});
}
}
//Set the gameTip to 0.
gameTipIndex=0;
// Limit the scope of bm, as it's a borrowed pointer.
{
//Pointer to the sdl texture that will contain a message, if any.
SDL_Texture* bm=NULL;
//Check if the player is dead, meaning we draw a message.
if(player.dead){
//Get user configured restart key
string keyCodeRestart = InputManagerKeyCode::describeTwo(inputMgr.getKeyCode(INPUTMGR_RESTART, false), inputMgr.getKeyCode(INPUTMGR_RESTART, true));
transform(keyCodeRestart.begin(),keyCodeRestart.end(),keyCodeRestart.begin(),::toupper);
//The player is dead, check if there's a state that can be loaded.
if(player.canLoadState()){
//Now check if the tip is already made, if not make it.
if(bmTips[3]==NULL){
//Get user defined key for loading checkpoint
string keyCodeLoad = InputManagerKeyCode::describeTwo(inputMgr.getKeyCode(INPUTMGR_LOAD, false), inputMgr.getKeyCode(INPUTMGR_LOAD, true));
transform(keyCodeLoad.begin(),keyCodeLoad.end(),keyCodeLoad.begin(),::toupper);
//Draw string
bmTips[3]=textureFromText(renderer, *fontText,//TTF_RenderUTF8_Blended(fontText,
/// TRANSLATORS: Please do not remove %s from your translation:
/// - first %s means currently configured key to restart game
/// - Second %s means configured key to load from last save
tfm::format(_("Press %s to restart current level or press %s to load the game."),
keyCodeRestart,keyCodeLoad).c_str(),
objThemes.getTextColor(true));
}
bm=bmTips[3].get();
}else{
//Now check if the tip is already made, if not make it.
if(bmTips[2]==NULL){
bmTips[2]=textureFromText(renderer, *fontText,
/// TRANSLATORS: Please do not remove %s from your translation:
/// - %s will be replaced with currently configured key to restart game
tfm::format(_("Press %s to restart current level."),keyCodeRestart).c_str(),
objThemes.getTextColor(true));
}
bm=bmTips[2].get();
}
}
//Check if the shadow has died (and there's no other notification).
//NOTE: We use the shadow's jumptime as countdown, this variable isn't used when the shadow is dead.
if(shadow.dead && bm==NULL && shadow.jumpTime>0){
//Now check if the tip is already made, if not make it.
if(bmTips[1]==NULL){
bmTips[1]=textureFromText(renderer, *fontText,
_("Your shadow has died."),
objThemes.getTextColor(true));
}
bm=bmTips[1].get();
//NOTE: Logic in the render loop, we substract the shadow's jumptime by one.
shadow.jumpTime--;
//return view to player and keep it there
cameraMode=CAMERA_PLAYER;
}
//Draw the tip.
if(bm!=NULL){
const SDL_Rect textureSize = rectFromTexture(*bm);
int x=(SCREEN_WIDTH-textureSize.w)/2;
int y=32;
drawGUIBox(x-8,y-8,textureSize.w+16,textureSize.h+14,renderer,0xFFFFFFFF);
applyTexture(x,y,*bm,renderer);
}
}
//Show the number of collectables the user has collected if there are collectables in the level.
//We hide this when interlevel.
if(currentCollectables<=totalCollectables && totalCollectables!=0 && !interlevel && time>0){
if(collectablesTexture.needsUpdate(currentCollectables)) {
- //Temp stringstream just to addup all the text nicely
- std::stringstream temp;
- temp << currentCollectables << "/" << totalCollectables;
collectablesTexture.update(currentCollectables,
textureFromText(renderer,
*fontText,
- temp.str().c_str(),
+ tfm::format("%d/%d", currentCollectables, totalCollectables).c_str(),
objThemes.getTextColor(true)));
}
SDL_Rect bmSize = rectFromTexture(*collectablesTexture.get());
//Draw background
drawGUIBox(SCREEN_WIDTH-bmSize.w-34,SCREEN_HEIGHT-bmSize.h-4,bmSize.w+34+2,bmSize.h+4+2,renderer,0xFFFFFFFF);
//Draw the collectable icon
collectable.draw(renderer,SCREEN_WIDTH-50+12,SCREEN_HEIGHT-50+10);
//Draw text
applyTexture(SCREEN_WIDTH-50-bmSize.w+22,SCREEN_HEIGHT-bmSize.h,collectablesTexture.getTexture(),renderer);
}
//show time and records used in level editor or during replay.
if((stateID==STATE_LEVEL_EDITOR || (!interlevel && player.isPlayFromRecord())) && time>0){
const SDL_Color fg=objThemes.getTextColor(true),bg={255,255,255,255};
const int alpha = 160;
if (recordingsTexture.needsUpdate(recordings)) {
recordingsTexture.update(recordings,
textureFromTextShaded(
renderer,
*fontText,
tfm::format(ngettext("%d recording","%d recordings",recordings).c_str(),recordings).c_str(),
fg,
bg
));
SDL_SetTextureAlphaMod(recordingsTexture.get(),alpha);
}
int y=SCREEN_HEIGHT - textureHeight(*recordingsTexture.get());
if (stateID != STATE_LEVEL_EDITOR && bmTips[0] != NULL && !interlevel) {
y -= textureHeight(bmTips[0]) + 4;
}
applyTexture(0,y,*recordingsTexture.get(), renderer);
if(timeTexture.needsUpdate(time)) {
const size_t len = 32;
- char c[len];
- SDL_snprintf(c,len,"%-.2fs",time/40.0);
timeTexture.update(time,
textureFromTextShaded(
renderer,
*fontText,
- c,
+ tfm::format("%-.2fs", time / 40.0).c_str(),
fg,
bg
));
}
y -= textureHeight(*timeTexture.get());
applyTexture(0,y,*timeTexture.get(), renderer);
}
//Draw the current action in the upper right corner.
if(player.record){
const SDL_Rect r = { 0, 0, 50, 50 };
applyTexture(SCREEN_WIDTH - 50, 0, *action, renderer, &r);
} else if (shadow.state != 0){
const SDL_Rect r={50,0,50,50};
applyTexture(SCREEN_WIDTH-50,0,*action,renderer,&r);
}
//if the game is play from record then draw something indicates it
if(player.isPlayFromRecord()){
//Dim the screen if interlevel is true.
if( interlevel){
dimScreen(renderer,191);
}else if((time & 0x10)==0x10){
// FIXME: replace this ugly ad-hoc animation by a better one
const SDL_Rect r={50,0,50,50};
applyTexture(0,0,*action,renderer,&r);
//applyTexture(0,SCREEN_HEIGHT-50,*action,renderer,&r);
//applyTexture(SCREEN_WIDTH-50,SCREEN_HEIGHT-50,*action,renderer,&r);
}
}else if(player.objNotificationBlock){
//If the player is in front of a notification block show the message.
//And it isn't a replay.
//Check if we need to update the notification message texture.
const auto& blockId = player.objNotificationBlock;
int maxWidth = 0;
int y = 20;
//We check against blockId rather than the full message, as blockId is most likely shorter.
if(notificationTexture.needsUpdate(blockId)) {
const std::string &untranslated_message=player.objNotificationBlock->message;
std::string message=_CC(levels->getDictionaryManager(),untranslated_message);
std::vector<std::string> string_data;
//Trim the message.
{
size_t lps = message.find_first_not_of("\n\r \t");
if (lps == string::npos) {
message.clear(); // it's completely empty
} else {
message = message.substr(lps, message.find_last_not_of("\n\r \t") - lps + 1);
}
}
//Split the message into lines.
for (int lps = 0;;) {
// determine the end of line
int lpe = lps;
for (; message[lpe] != '\n' && message[lpe] != '\r' && message[lpe] != '\0'; lpe++);
string_data.push_back(message.substr(lps, lpe - lps));
// break if the string ends
if (message[lpe] == '\0') break;
// skip "\r\n" for Windows line ending
if (message[lpe] == '\r' && message[lpe + 1] == '\n') lpe++;
// point to the start of next line
lps = lpe + 1;
}
vector<SurfacePtr> lines;
//Create the image for each lines
for (int i = 0; i < (int)string_data.size(); i++) {
//Integer used to center the sentence horizontally.
int x = 0;
TTF_SizeUTF8(fontText, string_data[i].c_str(), &x, NULL);
//Find out largest width
if (x>maxWidth)
maxWidth = x;
lines.emplace_back(TTF_RenderUTF8_Blended(fontText, string_data[i].c_str(), objThemes.getTextColor(true)));
//Increase y with 25, about the height of the text.
y += 25;
}
maxWidth+=SCREEN_WIDTH*0.15;
SurfacePtr surf = createSurface(maxWidth, y);
int y1 = y;
for(SurfacePtr &s : lines) {
if(s) {
applySurface((surf->w-s->w)/2,surf->h - y1,s.get(),surf.get(),NULL);
}
y1 -= 25;
}
notificationTexture.update(blockId, textureUniqueFromSurface(renderer,std::move(surf)));
} else {
auto texSize = rectFromTexture(*notificationTexture.get());
maxWidth=texSize.w;
y=texSize.h;
}
drawGUIBox((SCREEN_WIDTH-maxWidth)/2,SCREEN_HEIGHT-y-25,maxWidth,y+20,renderer,0xFFFFFFBF);
applyTexture((SCREEN_WIDTH-maxWidth)/2,SCREEN_HEIGHT-y,notificationTexture.getTexture(),renderer);
}
}
void Game::resize(ImageManager&, SDL_Renderer& /*renderer*/){
//Check if the interlevel popup is shown.
if(interlevel && GUIObjectRoot){
GUIObjectRoot->left=(SCREEN_WIDTH-GUIObjectRoot->width)/2;
}
}
void Game::replayPlay(ImageManager& imageManager,SDL_Renderer& renderer){
//Set interlevel true.
interlevel=true;
//Make a copy of the playerButtons.
vector<int> recordCopy=player.recordButton;
//Reset the game.
//NOTE: We don't reset the saves. I'll see that if it will introduce bugs.
reset(false, false);
//Make the cursor visible when the interlevel popup is up.
SDL_ShowCursor(SDL_ENABLE);
//Set the copy of playerButtons back.
player.recordButton=recordCopy;
//Now play the recording.
player.playRecord();
//Create the gui if it isn't already done.
if(!GUIObjectRoot){
//Create a new GUIObjectRoot the size of the screen.
GUIObjectRoot=new GUIObject(imageManager,renderer,0,0,SCREEN_WIDTH,SCREEN_HEIGHT);
//Make child widgets change color properly according to theme.
GUIObjectRoot->inDialog=true;
//Create a GUIFrame for the upper frame.
GUIFrame* upperFrame=new GUIFrame(imageManager,renderer,0,4,0,74);
GUIObjectRoot->addChild(upperFrame);
//Render the You've finished: text and add it to a GUIImage.
//NOTE: The texture is managed by the GUIImage so no need to free it ourselfs.
auto bm = SharedTexture(textureFromText(renderer, *fontGUI,_("You've finished:"),objThemes.getTextColor(true)));
const SDL_Rect textureSize = rectFromTexture(*bm);
GUIImage* title=new GUIImage(imageManager,renderer,0,4-GUI_FONT_RAISE,textureSize.w,textureSize.h,bm);
upperFrame->addChild(title);
//Create the sub title.
string s;
if (levels->getLevelCount()>0){
/// TRANSLATORS: Please do not remove %s or %d from your translation:
/// - %d means the level number in a levelpack
/// - %s means the name of current level
s=tfm::format(_("Level %d %s"),levels->getCurrentLevel()+1,_CC(levels->getDictionaryManager(),levelName));
}
GUIObject* obj=new GUILabel(imageManager,renderer,0,44,0,28,s.c_str(),0,true,true,GUIGravityCenter);
upperFrame->addChild(obj);
obj->render(renderer,0,0,false);
//Determine the width the upper frame should have.
int width;
if(textureSize.w>obj->width)
width=textureSize.w+32;
else
width=obj->width+32;
//Set the left of the title.
title->left=(width-title->width)/2;
//Set the width of the level label to the width of the frame for centering.
obj->width=width;
//Now set the position and width of the frame.
upperFrame->width=width;
upperFrame->left=(SCREEN_WIDTH-width)/2;
//Now create a GUIFrame for the lower frame.
GUIFrame* lowerFrame=new GUIFrame(imageManager,renderer,0,SCREEN_HEIGHT-140,570,135);
GUIObjectRoot->addChild(lowerFrame);
//The different values.
int bestTime=levels->getLevel()->time;
int targetTime=levels->getLevel()->targetTime;
int bestRecordings=levels->getLevel()->recordings;
int targetRecordings=levels->getLevel()->targetRecordings;
int medal=1;
if(bestTime>=0 && (targetTime<0 || bestTime<=targetTime))
medal++;
if(bestRecordings>=0 && (targetRecordings<0 || bestRecordings<=targetRecordings))
medal++;
int maxWidth=0;
int x=20;
//Is there a target time for this level?
int timeY=0;
bool isTargetTime=true;
if(targetTime<0){
isTargetTime=false;
timeY=12;
}
//Create the labels with the time and best time.
/// TRANSLATORS: Please do not remove %-.2f from your translation:
/// - %-.2f means time in seconds
/// - s is shortened form of a second. Try to keep it so.
obj=new GUILabel(imageManager,renderer,x,10+timeY,-1,36,tfm::format(_("Time: %-.2fs"),time/40.0).c_str());
lowerFrame->addChild(obj);
obj->render(renderer,0,0,false);
maxWidth=obj->width;
/// TRANSLATORS: Please do not remove %-.2f from your translation:
/// - %-.2f means time in seconds
/// - s is shortened form of a second. Try to keep it so.
obj=new GUILabel(imageManager,renderer,x,34+timeY,-1,36,tfm::format(_("Best time: %-.2fs"),bestTime/40.0).c_str());
lowerFrame->addChild(obj);
obj->render(renderer,0,0,false);
if(obj->width>maxWidth)
maxWidth=obj->width;
/// TRANSLATORS: Please do not remove %-.2f from your translation:
/// - %-.2f means time in seconds
/// - s is shortened form of a second. Try to keep it so.
if(isTargetTime){
obj=new GUILabel(imageManager,renderer,x,58,-1,36,tfm::format(_("Target time: %-.2fs"),targetTime/40.0).c_str());
lowerFrame->addChild(obj);
obj->render(renderer,0,0,false);
if(obj->width>maxWidth)
maxWidth=obj->width;
}
x+=maxWidth+20;
//Is there target recordings for this level?
int recsY=0;
bool isTargetRecs=true;
if(targetRecordings<0){
isTargetRecs=false;
recsY=12;
}
//Now the ones for the recordings.
/// TRANSLATORS: Please do not remove %d from your translation:
/// - %d means the number of recordings user has made
obj=new GUILabel(imageManager,renderer,x,10+recsY,-1,36,tfm::format(_("Recordings: %d"),recordings).c_str());
lowerFrame->addChild(obj);
obj->render(renderer,0,0,false);
maxWidth=obj->width;
/// TRANSLATORS: Please do not remove %d from your translation:
/// - %d means the number of recordings user has made
obj=new GUILabel(imageManager,renderer,x,34+recsY,-1,36,tfm::format(_("Best recordings: %d"),bestRecordings).c_str());
lowerFrame->addChild(obj);
obj->render(renderer,0,0,false);
if(obj->width>maxWidth)
maxWidth=obj->width;
/// TRANSLATORS: Please do not remove %d from your translation:
/// - %d means the number of recordings user has made
if(isTargetRecs){
obj=new GUILabel(imageManager,renderer,x,58,-1,36,tfm::format(_("Target recordings: %d"),targetRecordings).c_str());
lowerFrame->addChild(obj);
obj->render(renderer,0,0,false);
if(obj->width>maxWidth)
maxWidth=obj->width;
}
x+=maxWidth;
//The medal that is earned.
/// TRANSLATORS: Please do not remove %s from your translation:
/// - %s will be replaced with name of a prize medal (gold, silver or bronze)
string s1=tfm::format(_("You earned the %s medal"),(medal>1)?(medal==3)?_("GOLD"):_("SILVER"):_("BRONZE"));
obj=new GUILabel(imageManager,renderer,50,92,-1,36,s1.c_str(),0,true,true,GUIGravityCenter);
lowerFrame->addChild(obj);
obj->render(renderer,0,0,false);
if(obj->left+obj->width>x){
x=obj->left+obj->width+30;
}else{
obj->left=20+(x-20-obj->width)/2;
}
//Create the rectangle for the earned medal.
SDL_Rect r;
r.x=(medal-1)*30;
r.y=0;
r.w=30;
r.h=30;
//Create the medal on the left side.
obj=new GUIImage(imageManager,renderer,16,92,30,30,medals,r);
lowerFrame->addChild(obj);
//And the medal on the right side.
obj=new GUIImage(imageManager,renderer,x-24,92,30,30,medals,r);
lowerFrame->addChild(obj);
//Create the three buttons, Menu, Restart, Next.
/// TRANSLATORS: used as return to the level selector menu
GUIObject* b1=new GUIButton(imageManager,renderer,x,10,-1,36,_("Menu"),0,true,true,GUIGravityCenter);
b1->name="cmdMenu";
b1->eventCallback=this;
lowerFrame->addChild(b1);
b1->render(renderer,0,0,true);
/// TRANSLATORS: used as restart level
GUIObject* b2=new GUIButton(imageManager,renderer,x,50,-1,36,_("Restart"),0,true,true,GUIGravityCenter);
b2->name="cmdRestart";
b2->eventCallback=this;
lowerFrame->addChild(b2);
b2->render(renderer,0,0,true);
/// TRANSLATORS: used as next level
GUIObject* b3=new GUIButton(imageManager,renderer,x,90,-1,36,_("Next"),0,true,true,GUIGravityCenter);
b3->name="cmdNext";
b3->eventCallback=this;
lowerFrame->addChild(b3);
b3->render(renderer,0,0,true);
maxWidth=b1->width;
if(b2->width>maxWidth)
maxWidth=b2->width;
if(b3->width>maxWidth)
maxWidth=b3->width;
b1->left=b2->left=b3->left=x+maxWidth/2;
x+=maxWidth;
lowerFrame->width=x;
lowerFrame->left=(SCREEN_WIDTH-lowerFrame->width)/2;
}
}
void Game::recordingEnded(ImageManager& imageManager, SDL_Renderer& renderer){
//Check if it's a normal replay, if so just stop.
if(!interlevel){
//Show the cursor so that the user can press the ok button.
SDL_ShowCursor(SDL_ENABLE);
//Now show the message box.
msgBox(imageManager,renderer,_("Game replay is done."),MsgBoxOKOnly,_("Game Replay"));
//Go to the level select menu.
setNextState(STATE_LEVEL_SELECT);
//And change the music back to the menu music.
getMusicManager()->playMusic("menu");
}else{
//Instead of directly replaying we set won true to let the Game handle the replaying at the end of the update cycle.
won=true;
}
}
bool Game::canSaveState(){
return (player.canSaveState() && shadow.canSaveState());
}
bool Game::saveState(){
//Check if the player and shadow can save the current state.
if(canSaveState()){
//Let the player and the shadow save their state.
player.saveState();
shadow.saveState();
//Save the stats.
timeSaved=time;
recordingsSaved=recordings;
recentSwapSaved=recentSwap;
//Save the camera mode and target.
cameraModeSaved=cameraMode;
cameraTargetSaved=cameraTarget;
//Save the current collectables
currentCollectablesSaved=currentCollectables;
//Save scripts.
copyCompiledScripts(getScriptExecutor()->getLuaState(), compiledScripts, savedCompiledScripts);
//Save other state, for example moving blocks.
for (auto block : levelObjects){
block->saveState();
copyCompiledScripts(getScriptExecutor()->getLuaState(), block->compiledScripts, block->savedCompiledScripts);
}
//Also save states of scenery layers.
for (auto it = sceneryLayers.begin(); it != sceneryLayers.end(); ++it) {
it->second->saveAnimation();
}
//Also save the background animation, if any.
if(background)
background->saveAnimation();
if(!player.isPlayFromRecord() && !interlevel){
//Update achievements
Uint32 t=SDL_GetTicks()+5000; //Add a bias to prevent bugs
if(recentSave+1000>t){
statsMgr.newAchievement("panicSave");
}
recentSave=t;
//Update statistics.
statsMgr.saveTimes++;
//Update achievements
switch(statsMgr.saveTimes){
case 1000:
statsMgr.newAchievement("save1k");
break;
}
}
//Save the state for script executor.
getScriptExecutor()->saveState();
//Execute the onSave event.
executeScript(LevelEvent_OnSave);
//Return true.
return true;
}
//We can't save the state so return false.
return false;
}
bool Game::loadState(){
//Check if there's a state that can be loaded.
if(player.canLoadState() && shadow.canLoadState()){
//Let the player and the shadow load their state.
player.loadState();
shadow.loadState();
//Load the stats.
time=timeSaved;
recordings=recordingsSaved;
recentSwap=recentSwapSaved;
//Load the camera mode and target.
cameraMode=cameraModeSaved;
cameraTarget=cameraTargetSaved;
//Load the current collactbles
currentCollectables=currentCollectablesSaved;
//Load scripts.
copyCompiledScripts(getScriptExecutor()->getLuaState(), savedCompiledScripts, compiledScripts);
//Load other state, for example moving blocks.
for(auto block:levelObjects){
block->loadState();
copyCompiledScripts(getScriptExecutor()->getLuaState(), block->savedCompiledScripts, block->compiledScripts);
}
//Also load states of scenery layers.
for (auto it = sceneryLayers.begin(); it != sceneryLayers.end(); ++it) {
it->second->loadAnimation();
}
//Also load the background animation, if any.
if(background)
background->loadAnimation();
if(!player.isPlayFromRecord() && !interlevel){
//Update achievements.
Uint32 t=SDL_GetTicks()+5000; //Add a bias to prevent bugs
if(recentLoad+1000>t){
statsMgr.newAchievement("panicLoad");
}
recentLoad=t;
//Update statistics.
statsMgr.loadTimes++;
//Update achievements
switch(statsMgr.loadTimes){
case 1000:
statsMgr.newAchievement("load1k");
break;
}
}
//Load the state for script executor.
getScriptExecutor()->loadState();
//Execute the onLoad event, if any.
executeScript(LevelEvent_OnLoad);
//Return true.
return true;
}
//We can't load the state so return false.
return false;
}
void Game::reset(bool save,bool noScript){
//We need to reset the game so we also reset the player and the shadow.
player.reset(save);
shadow.reset(save);
saveStateNextTime=false;
loadStateNextTime=false;
//Reset the stats if interlevel isn't true.
if(!interlevel){
time=0;
recordings=0;
}
recentSwap=-10000;
if(save) recentSwapSaved=-10000;
//Reset the camera.
cameraMode=CAMERA_PLAYER;
if(save) cameraModeSaved=CAMERA_PLAYER;
cameraTarget.x=cameraTarget.y=cameraTarget.w=cameraTarget.h=0;
if(save) cameraTargetSaved.x=cameraTargetSaved.y=cameraTargetSaved.w=cameraTargetSaved.h=0;
//Reset the number of collectables
currentCollectables=0;
if(save)
currentCollectablesSaved=0;
//There is no last checkpoint so set it to NULL.
if(save)
objLastCheckPoint=NULL;
//Clear the event queue, since all the events are from before the reset.
eventQueue.clear();
//Reset other state, for example moving blocks.
for(unsigned int i=0;i<levelObjects.size();i++){
levelObjects[i]->reset(save);
}
//Also reset states of scenery layers.
for (auto it = sceneryLayers.begin(); it != sceneryLayers.end(); ++it) {
it->second->resetAnimation(save);
}
//Also reset the background animation, if any.
if(background)
background->resetAnimation(save);
//Reset the cached notification block
notificationTexture.update(NULL, NULL);
//Reset the script environment if necessary.
if (noScript) {
//Destroys the script environment completely.
getScriptExecutor()->destroy();
//Clear the level script.
compiledScripts.clear();
savedCompiledScripts.clear();
initialCompiledScripts.clear();
//Clear the block script.
for (auto block : levelObjects){
block->compiledScripts.clear();
block->savedCompiledScripts.clear();
block->initialCompiledScripts.clear();
}
} else {
if (save || getScriptExecutor()->getLuaState() == NULL) {
//Create a new script environment.
getScriptExecutor()->reset(true);
//Recompile the level script.
compiledScripts.clear();
savedCompiledScripts.clear();
initialCompiledScripts.clear();
for (auto it = scripts.begin(); it != scripts.end(); ++it){
int index = getScriptExecutor()->compileScript(it->second);
compiledScripts[it->first] = index;
lua_rawgeti(getScriptExecutor()->getLuaState(), LUA_REGISTRYINDEX, index);
initialCompiledScripts[it->first] = luaL_ref(getScriptExecutor()->getLuaState(), LUA_REGISTRYINDEX);
}
//Recompile the block script.
for (auto block : levelObjects) {
block->compiledScripts.clear();
block->savedCompiledScripts.clear();
block->initialCompiledScripts.clear();
for (auto it = block->scripts.begin(); it != block->scripts.end(); ++it){
int index = getScriptExecutor()->compileScript(it->second);
block->compiledScripts[it->first] = index;
lua_rawgeti(getScriptExecutor()->getLuaState(), LUA_REGISTRYINDEX, index);
block->initialCompiledScripts[it->first] = luaL_ref(getScriptExecutor()->getLuaState(), LUA_REGISTRYINDEX);
}
}
} else {
//Do a soft reset.
getScriptExecutor()->reset(false);
//Restore the level script to initial state.
copyCompiledScripts(getScriptExecutor()->getLuaState(), initialCompiledScripts, compiledScripts);
//Restore the block script to initial state.
for (auto block : levelObjects) {
copyCompiledScripts(getScriptExecutor()->getLuaState(), block->initialCompiledScripts, block->compiledScripts);
}
}
}
//Call the level's onCreate event.
executeScript(LevelEvent_OnCreate);
//Send GameObjectEvent_OnCreate event to the script
for (auto block : levelObjects) {
block->onEvent(GameObjectEvent_OnCreate);
}
//Close exit(s) if there are any collectables
if (totalCollectables>0){
for (auto block : levelObjects){
if (block->type == TYPE_EXIT){
block->onEvent(GameObjectEvent_OnSwitchOff);
}
}
}
//Hide the cursor (if not the leveleditor).
if(stateID!=STATE_LEVEL_EDITOR)
SDL_ShowCursor(SDL_DISABLE);
}
void Game::executeScript(int eventType){
map<int,int>::iterator it;
//Check if there's a script for the given event.
it=compiledScripts.find(eventType);
if(it!=compiledScripts.end()){
//There is one so execute it.
getScriptExecutor()->executeScript(it->second);
}
}
void Game::broadcastObjectEvent(int eventType,int objectType,const char* id,GameObject* target){
//Create a typeGameObjectEvent that can be put into the queue.
typeGameObjectEvent e;
//Set the event type.
e.eventType=eventType;
//Set the object type.
e.objectType=objectType;
//By default flags=0.
e.flags=0;
//Unless there's an id.
if(id){
//Set flags to 0x1 and set the id.
e.flags|=1;
e.id=id;
}
//Or there's a target given.
if(target)
e.target=target;
else
e.target=NULL;
//Add the event to the queue.
eventQueue.push_back(e);
}
void Game::getCurrentLevelAutoSaveRecordPath(std::string &bestTimeFilePath,std::string &bestRecordingFilePath,bool createPath){
levels->getLevelAutoSaveRecordPath(-1,bestTimeFilePath,bestRecordingFilePath,createPath);
}
void Game::gotoNextLevel(ImageManager& imageManager, SDL_Renderer& renderer){
//Goto the next level.
levels->nextLevel();
//Check if the level exists.
if(levels->getCurrentLevel()<levels->getLevelCount()){
setNextState(STATE_GAME);
}else{
if(!levels->congratulationText.empty()){
msgBox(imageManager,renderer,_CC(levels->getDictionaryManager(),levels->congratulationText),MsgBoxOKOnly,_("Congratulations"));
}else{
msgBox(imageManager,renderer,_("You have finished the levelpack!"),MsgBoxOKOnly,_("Congratulations"));
}
//Now go back to the levelselect screen.
setNextState(STATE_LEVEL_SELECT);
//And set the music back to menu.
getMusicManager()->playMusic("menu");
}
}
void Game::GUIEventCallback_OnEvent(ImageManager& imageManager,SDL_Renderer& renderer, string name,GUIObject* obj,int eventType){
if(name=="cmdMenu"){
setNextState(STATE_LEVEL_SELECT);
//And change the music back to the menu music.
getMusicManager()->playMusic("menu");
}else if(name=="cmdRestart"){
//Clear the gui.
if(GUIObjectRoot){
delete GUIObjectRoot;
GUIObjectRoot=NULL;
}
interlevel=false;
//And reset the game.
//NOTE: We don't need to clear the save game because in level replay the game won't be saved (??)
//TODO: it seems work (??); I'll see if it introduce bugs
reset(false, false);
}else if(name=="cmdNext"){
//No matter what, clear the gui.
if(GUIObjectRoot){
delete GUIObjectRoot;
GUIObjectRoot=NULL;
}
//And goto the next level.
gotoNextLevel(imageManager,renderer);
}
}
diff --git a/src/Globals.h b/src/Globals.h
index 4487ca4..e019f08 100644
--- a/src/Globals.h
+++ b/src/Globals.h
@@ -1,263 +1,263 @@
/*
* Copyright (C) 2011-2013 Me and My Shadow
*
* This file is part of Me and My Shadow.
*
* Me and My Shadow is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Me and My Shadow is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Me and My Shadow. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef GLOBALS_H
#define GLOBALS_H
#include <SDL.h>
#include <string>
#include "libs/tinygettext/tinygettext.hpp"
#include "LevelPack.h"
#include "Render.h"
#if defined (WIN32) || defined (__APPLE__)
//#define DATA_PATH
#else
#include "config.h"
#endif
#define TITLE_FONT_RAISE 19
#define GUI_FONT_RAISE 5
class GameState;
//Global constants
//The width of the screen.
extern int SCREEN_WIDTH;
//The height of the screen.
extern int SCREEN_HEIGHT;
//The depth of the screen.
#if defined(ANDROID)
//TODO: change other surface creating code to make the game runs faster
const int SCREEN_BPP=16; //??? 24?? 32??
//const int SCREEN_FLAGS=SDL_HWSURFACE;
#else
const int SCREEN_BPP=32;
//const int SCREEN_FLAGS=SDL_HWSURFACE;
#endif
const int SCREEN_FLAGS = 0;
//SDL interprets each pixel as a 32-bit number,
// so our masks must depend on the endianness (byte order) of the machine.
//NOTE: We define them here so we only have to do it once.
/*#if SDL_BYTEORDER == SDL_BIG_ENDIAN
const Uint32 BMASK=0xFF000000;
const Uint32 GMASK=0x00FF0000;
const Uint32 RMASK=0x0000FF00;
const Uint32 AMASK=0x000000FF;
#else*/
// NOTE: Changed to ARGB for SDL2.
const Uint32 BMASK=0x000000FF;
const Uint32 GMASK=0x0000FF00;
const Uint32 RMASK=0x00FF0000;
const Uint32 AMASK=0xFF000000;
/*#endif*/
//String containing the version, used in the titelbar.
//NOTE: for development version please write something like "V0.5 Development version"
//which can trigger the corresponding achievement.
-const std::string version = "V0.5 RC2";
+const std::string version = "V0.5";
//The height of the current level.
extern int LEVEL_HEIGHT;
//The width of the current level.
extern int LEVEL_WIDTH;
//The target frames per seconds.
const int FPS=40;
//The language that in which the game should be translated.
extern std::string language;
//The DictionaryManager that is used to translate the game itself.
extern tinygettext::DictionaryManager* dictionaryManager;
//SDL Window and renderer
extern SDL_Window* sdlWindow;
//Font that is used for titles.
//Knewave large.
extern TTF_Font* fontTitle;
//Font that is used for captions of buttons and other GUI elements.
//Knewave small.
extern TTF_Font* fontGUI;
//Font that is used for long captions of buttons and other GUI elements.
//Knewave smaller.
extern TTF_Font* fontGUISmall;
//Font that is used for (long) text.
//Blokletter-Viltstift small.
extern TTF_Font* fontText;
//Font used for scripting editor.
//Monospace.
extern TTF_Font* fontMono;
//Small arrows used for GUI widgets.
//2 directions and 2 different/same colors depending on theme.
extern TexturePtr arrowLeft1;
extern TexturePtr arrowRight1;
extern TexturePtr arrowLeft2;
extern TexturePtr arrowRight2;
//Event, used for event handling.
extern SDL_Event event;
//GUI
class GUIObject;
extern GUIObject *GUIObjectRoot;
//The state id of the current state.
extern int stateID;
//Integer containing what the next state will be.
extern int nextState;
//The currentState.
extern GameState* currentState;
//Pointer to the current levelpack.
extern LevelPack* levels;
//String containing the name of the current level.
extern std::string levelName;
//SDL rectangle used to store the camera.
//x is the x location of the camera.
//y is the y location of the camera.
//w is the width of the camera. (equal to SCREEN_WIDTH)
//h is the height of the camera. (equal to SCREEN_HEIGHT)
extern SDL_Rect camera;
//Themable colors
const SDL_Color BLACK = SDL_Color{0,0,0,255};
//Enumeration containing the different cursor types there are.
enum CursorType{
//The default pointer.
CURSOR_POINTER,
//The vertical ibeam, used to indicate text input.
CURSOR_CARROT,
//A closed hand, used for indicating a drag action.
CURSOR_DRAG,
//The different (window) size cursor icons.
CURSOR_SIZE_HOR,
CURSOR_SIZE_VER,
CURSOR_SIZE_FDIAG,
CURSOR_SIZE_BDIAG,
//Remove cursor used in level editor
CURSOR_REMOVE,
//Pointing hand cursor, for hyperlinks.
CURSOR_POINTING_HAND,
//The number of cursor types there are.
CURSOR_MAX
};
//Currently used cursor type.
extern CursorType currentCursor;
//Array containing the SDL_Cursors.
extern SDL_Cursor* cursors[CURSOR_MAX];
//Enumeration containing the ids of the game states.
enum GameStates{
//State null is a special state used to indicate no state.
//This is used when no next state is defined.
STATE_NULL,
//This state is before the actual leveleditor used to make levelpacks.
STATE_LEVEL_EDIT_SELECT,
//This state is for the level editor.
STATE_LEVEL_EDITOR,
//This state is for the main menu.
STATE_MENU,
//This state is for the actual game.
STATE_GAME,
//Special state used when exiting meandmyshadow.
STATE_EXIT,
//This state is for the help screen.
STATE_LEVEL_SELECT,
//This state is for the options screen.
STATE_OPTIONS,
//This state is for the addon screen.
STATE_ADDONS,
//This state is for credits screen
STATE_CREDITS,
//This state is for statistics screen
STATE_STATISTICS,
};
//Enumeration containing the ids of the different block types.
enum GameTileType{
//The normal solid block.
TYPE_BLOCK=0,
//Block representing the start location of the player.
TYPE_START_PLAYER,
//Block representing the start location of the shadow.
TYPE_START_SHADOW,
//The exit of the level.
TYPE_EXIT,
//The shadow block which is only solid for the shadow.
TYPE_SHADOW_BLOCK,
//Block that can kill both the player and the shadow.
TYPE_SPIKES,
//Special point where the player can save.
TYPE_CHECKPOINT,
//Block that will switch the location of the player and the shadow when invoked.
TYPE_SWAP,
//Block that will crumble to dust when stepped on it for the third time.
TYPE_FRAGILE,
//Normal block that moves along a path.
TYPE_MOVING_BLOCK,
//Shadow block that moves along a path.
TYPE_MOVING_SHADOW_BLOCK,
//A spike block that moves along a path.
TYPE_MOVING_SPIKES,
//Special block which, once entered, moves the player/shadow to a different portal.
TYPE_PORTAL,
//A block with a button which can activate or stop moving blocks, converyor belts
TYPE_BUTTON,
//A switch which can activate or stop moving blocks, converyor belts
TYPE_SWITCH,
//Solid block which works like
TYPE_CONVEYOR_BELT,
TYPE_SHADOW_CONVEYOR_BELT,
//Block that contains a message that can be read.
TYPE_NOTIFICATION_BLOCK,
//A collectable that is able to open locked doors
TYPE_COLLECTABLE,
//Block that can be pushed by the player and the shadow.
//Pushable blocks can push other pushable blocks.
TYPE_PUSHABLE,
//The (max) number of tiles.
TYPE_MAX
};
//Keyboard only mode. This is set to true if the last menu navigation is performed by keyboard.
extern bool isKeyboardOnly;
#endif
diff --git a/src/LevelEditor.cpp b/src/LevelEditor.cpp
index 977dc19..8fc570b 100644
--- a/src/LevelEditor.cpp
+++ b/src/LevelEditor.cpp
@@ -1,4922 +1,4928 @@
/*
* Copyright (C) 2011-2013 Me and My Shadow
*
* This file is part of Me and My Shadow.
*
* Me and My Shadow is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Me and My Shadow is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Me and My Shadow. If not, see <http://www.gnu.org/licenses/>.
*/
#include "Block.h"
#include "Commands.h"
#include "CommandManager.h"
#include "GameState.h"
#include "Functions.h"
#include "GameObjects.h"
#include "ThemeManager.h"
#include "LevelPack.h"
#include "LevelPackManager.h"
#include "LevelEditor.h"
#include "TreeStorageNode.h"
#include "POASerializer.h"
#include "GUIListBox.h"
#include "GUITextArea.h"
#include "GUIWindow.h"
#include "GUISpinBox.h"
#include "MusicManager.h"
#include "InputManager.h"
#include "StatisticsManager.h"
#include <fstream>
#include <iostream>
#include <vector>
#include <algorithm>
#include <string.h>
#include <stdio.h>
#include <sstream>
#include <SDL_ttf.h>
#include "libs/tinyformat/tinyformat.h"
using namespace std;
static int levelTime,levelRecordings;
//Array containing translateble block names
static const char* blockNames[TYPE_MAX]={
__("Block"),__("Player Start"),__("Shadow Start"),
__("Exit"),__("Shadow Block"),__("Spikes"),
__("Checkpoint"),__("Swap"),__("Fragile"),
__("Moving Block"),__("Moving Shadow Block"),__("Moving Spikes"),
__("Teleporter"),__("Button"),__("Switch"),
__("Conveyor Belt"),__("Shadow Conveyor Belt"),__("Notification Block"),__("Collectable"),__("Pushable")
};
static const std::array<const char*, static_cast<size_t>(ToolTips::TooltipMax)> tooltipNames = {
__("Select"), __("Add"), __("Delete"), __("Play"), "", "", __("Level settings"), __("Save level"), __("Back to menu"),
__("Select"), __("Delete"), __("Configure")
};
static const std::array<const char*, static_cast<size_t>(ToolTips::TooltipMax)> tooltipHotkey = {
"F2", "F3", "F4", "F5", "", "", "", "Ctrl+S", "",
"", "", ""
};
static const std::array<int, static_cast<size_t>(ToolTips::TooltipMax)> tooltipHotkey2 = {
-1, -1, -1, -1, -1, -1, INPUTMGR_TAB, -1, INPUTMGR_ESCAPE,
-1, -1, -1
};
//Array indicates if block is linkable
static const bool isLinkable[TYPE_MAX]={
false,false,false,
false,false,false,
false,false,false,
true,true,true,
true,true,true,
false,false,false,false
};
static std::string describeSceneryName(const std::string& name) {
//Check if the input is a valid block name.
if (name.size() > 8 && name.substr(name.size() - 8) == "_Scenery") {
auto it = Game::blockNameMap.find(name.substr(0, name.size() - 8));
if (it != Game::blockNameMap.end()){
return tfm::format(_("%s (Scenery)"), _(blockNames[it->second]));
}
}
return name;
}
/////////////////LevelEditorActionsPopup/////////////////
class LevelEditorActionsPopup:private GUIEventCallback{
private:
//The parent object.
LevelEditor* parent;
//The position and size of window.
SDL_Rect rect;
//Array containing the actions in this popup.
GUIListBox* actions;
//GUI image.
SDL_Surface* bmGUI;
//Pointer to the object the actions apply to.
GameObject* target;
//The behaviour names.
vector<string> behaviour;
//The fragile block states.
vector<string> states;
//The separator texture.
//NOTE: It's created only when it's used. So don't access it directly!
SharedTexture separatorTexture;
//Get or create a new separator texture.
SharedTexture getOrCreateSeparatorTexture(SDL_Renderer& renderer) {
if (!separatorTexture) {
//NOTE: we use an arbitrart width since it will be updated soon.
createSeparatorTexture(renderer, 16);
}
return separatorTexture;
}
void createSeparatorTexture(SDL_Renderer& renderer, int width) {
//create surface
SurfacePtr surface = createSurface(width, 5);
//draw horizontal separator
const SDL_Rect r0 = { 4, 1, width - 8, 1 };
const SDL_Rect r1 = { 4, 3, width - 8, 1 };
const SDL_Rect r2 = { 4, 2, width - 8, 1 };
Uint32 c0 = SDL_MapRGB(surface->format, 192, 192, 192);
Uint32 c2 = SDL_MapRGB(surface->format, 64, 64, 64);
SDL_FillRect(surface.get(), &r0, c0);
SDL_FillRect(surface.get(), &r1, c0);
SDL_FillRect(surface.get(), &r2, c2);
//over
separatorTexture = textureFromSurface(renderer, std::move(surface));
}
void updateSeparators(SDL_Renderer& renderer) {
createSeparatorTexture(renderer, rect.w);
for (unsigned int i = 0; i < actions->item.size(); i++) {
if (actions->item[i] == "-") {
actions->updateItem(renderer, i, "-", separatorTexture);
}
}
}
public:
SDL_Rect getRect(){
return rect;
}
void dismiss(){
//Remove the actionsPopup from the parent.
if(parent!=NULL && parent->actionsPopup==this){
parent->actionsPopup=NULL;
}
//And delete ourself.
delete this;
}
SharedTexture createItem(SDL_Renderer& renderer,const char* caption,int icon,bool grayed=false){
//FIXME: Add some sort of caching?
//We draw using surfaces and convert to a texture in the end for now.
SDL_Color color = objThemes.getTextColor(true);
if (grayed) {
color.r = 128 + color.r / 2;
color.g = 128 + color.g / 2;
color.b = 128 + color.b / 2;
}
SurfacePtr tip(TTF_RenderUTF8_Blended(fontText,caption,color));
SDL_SetSurfaceBlendMode(tip.get(), SDL_BLENDMODE_NONE);
//Create the surface, we add 16px to the width for an icon,
//plus 8px for the border to make it looks better.
SurfacePtr item = createSurface(tip->w + 24 + (icon >= 0x100 ? 24 : 0), 24);
//Draw the text on the item surface.
applySurface(24 + (icon >= 0x100 ? 24 : 0), 0, tip.get(), item.get(), NULL);
//Temporarily set the blend mode of bmGUI to none, which simply copies RGBA channels to destination.
SDL_BlendMode oldBlendMode = SDL_BLENDMODE_BLEND;
SDL_GetSurfaceBlendMode(bmGUI, &oldBlendMode);
SDL_SetSurfaceBlendMode(bmGUI, SDL_BLENDMODE_NONE);
//Check if we should draw an icon.
if(icon>0){
//Draw the check (or not).
SDL_Rect r={0,0,16,16};
r.x=((icon-1)%8)*16;
r.y=(((icon-1)/8)%8)*16;
applySurface(4,3,bmGUI,item.get(),&r);
}
//Check if we should draw a secondary icon.
if (icon >= 0x100) {
SDL_Rect r = { 0, 0, 16, 16 };
r.x = ((icon / 0x100 - 1) % 8) * 16;
r.y = (((icon / 0x100 - 1) / 8) % 8) * 16;
applySurface(28, 3, bmGUI, item.get(), &r);
}
//Reset the blend mode of bmGUI.
SDL_SetSurfaceBlendMode(bmGUI, oldBlendMode);
//Check if we should update the width., 8px extra on the width is for four pixels spacing on either side.
if(item->w+8>rect.w) {
rect.w=item->w+8;
}
return textureFromSurface(renderer, std::move(item));
}
void updateListBoxSize() {
//Update the size of the GUIListBox.
actions->width = rect.w;
actions->height = rect.h;
int x = rect.x, y = rect.y;
if (x>SCREEN_WIDTH - rect.w) x = SCREEN_WIDTH - rect.w;
else if (x<0) x = 0;
if (y>SCREEN_HEIGHT - rect.h) y = SCREEN_HEIGHT - rect.h;
else if (y<0) y = 0;
rect.x = x;
rect.y = y;
}
void updateItem(SDL_Renderer& renderer,int index,const char* action,const char* caption,int icon=0,bool grayed=false){
auto item=createItem(renderer,caption,icon,grayed);
actions->updateItem(renderer, index,action,item,!grayed);
//Update the size of the GUIListBox.
updateListBoxSize();
}
void addItem(SDL_Renderer& renderer,const char* action,const char* caption,int icon=0,bool grayed=false){
auto item=createItem(renderer,caption,icon,grayed);
actions->addItem(renderer,action,item,!grayed);
//Update the height.
rect.h += 24;
//Update the size of the GUIListBox.
updateListBoxSize();
}
void addSeparator(SDL_Renderer& renderer) {
actions->addItem(renderer, "-", getOrCreateSeparatorTexture(renderer), false);
//Update the height.
rect.h += 5;
}
LevelEditorActionsPopup(ImageManager& imageManager,SDL_Renderer& renderer,LevelEditor* parent, GameObject* target, int x=0, int y=0){
this->parent=parent;
this->target=target;
rect.x = x;
rect.y = y;
//NOTE: The size gets set in the addItem method, height is already four to prevent a scrollbar.
rect.w=0;
rect.h=4;
//Load the gui images.
bmGUI=imageManager.loadImage(getDataPath()+"gfx/gui.png");
//Create the behaviour vector.
behaviour.push_back(_("On"));
behaviour.push_back(_("Off"));
behaviour.push_back(_("Toggle"));
//Create the states list.
states.push_back(_("Complete"));
states.push_back(_("One step"));
states.push_back(_("Two steps"));
states.push_back(_("Gone"));
//TODO: The width should be based on the longest option.
//Create default actions.
//NOTE: Width and height are determined later on when the options are rendered.
actions=new GUIListBox(imageManager,renderer,0,0,0,0);
actions->eventCallback=this;
//Check if it's a block or not.
if(target!=NULL)
addBlockItems(renderer);
else
addLevelItems(renderer);
}
static std::string getRepeatModeName(int mode) {
switch (mode) {
case Scenery::NEGATIVE_INFINITY:
return _("Negative infinity");
case Scenery::ZERO:
return _("Zero");
case Scenery::LEVEL_SIZE:
return _("Level size");
case Scenery::POSITIVE_INFINITY:
return _("Positive infinity");
default:
return _("Default");
}
}
void addBlockItems(SDL_Renderer& renderer){
//Check if the block is selected or not.
std::vector<GameObject*>::iterator it;
it=find(parent->selection.begin(),parent->selection.end(),target);
if(it!=parent->selection.end())
addItem(renderer,"Deselect",_("Deselect"));
else
addItem(renderer,"Select",_("Select"));
addItem(renderer,"Delete",_("Delete"),8);
addSeparator(renderer);
Scenery *scenery = dynamic_cast<Scenery*>(target);
if (scenery) {
// it is scenery block
addItem(renderer, "RepeatMode0", tfm::format(_("Horizontal repeat start: %s"),
getRepeatModeName(scenery->repeatMode & 0xFF)).c_str(), 8 * 2 + 3);
addItem(renderer, "RepeatMode1", tfm::format(_("Horizontal repeat end: %s"),
getRepeatModeName((scenery->repeatMode >> 8) & 0xFF)).c_str(), 8 * 2 + 4);
addItem(renderer, "RepeatMode2", tfm::format(_("Vertical repeat start: %s"),
getRepeatModeName((scenery->repeatMode >> 16) & 0xFF)).c_str(), 8 * 3 + 3);
addItem(renderer, "RepeatMode3", tfm::format(_("Vertical repeat end: %s"),
getRepeatModeName((scenery->repeatMode >> 24) & 0xFF)).c_str(), 8 * 3 + 4);
if (scenery->sceneryName_.empty()) {
addSeparator(renderer);
addItem(renderer, "CustomScenery", _("Custom scenery"), 8 + 4);
}
return;
}
addItem(renderer, "Visible", _("Visible"), (target->getEditorProperty("visible") == "1") ? 2 : 1);
//Get the type of the target.
int type = target->type;
//Determine what to do depending on the type.
if(isLinkable[type]){
//Check if it's a moving block type or trigger.
if(type==TYPE_BUTTON || type==TYPE_SWITCH || type==TYPE_PORTAL){
addItem(renderer, "Link", _("Link"), 8 * 2 + 8);
addItem(renderer, "Remove Links", _("Remove Links"), 8 * 2 + 7);
//Check if it's a portal, which contains a automatic option, and triggers a behaviour one.
if(type==TYPE_PORTAL){
addItem(renderer,"Automatic",_("Automatic"),(target->getEditorProperty("automatic")=="1")?2:1);
}else{
//Get the current behaviour.
int currentBehaviour=2;
if(target->getEditorProperty("behaviour")=="on"){
currentBehaviour=0;
}else if(target->getEditorProperty("behaviour")=="off"){
currentBehaviour=1;
}
addItem(renderer, "Behaviour", tfm::format(_("Behavior: %s"), behaviour[currentBehaviour]).c_str());
}
}else{
addItem(renderer, "Path", _("Path"), 8 + 5);
addItem(renderer, "Remove Path", _("Remove Path"), 8 * 4 + 2);
addItem(renderer,"Activated",_("Activated"),(target->getEditorProperty("activated")=="1")?2:1);
addItem(renderer,"Looping",_("Looping"),(target->getEditorProperty("loop")=="1")?2:1);
}
}
//Check for a conveyor belt.
if(type==TYPE_CONVEYOR_BELT || type==TYPE_SHADOW_CONVEYOR_BELT){
addItem(renderer,"Activated",_("Activated"),(target->getEditorProperty("activated")=="1")?2:1);
addItem(renderer,"Speed",_("Speed"));
}
//Check if it's a fragile block.
if(type==TYPE_FRAGILE){
//Get the current state.
int currentState=atoi(target->getEditorProperty("state").c_str());
addItem(renderer, "State", tfm::format(_("State: %s"), states[currentState]).c_str());
}
//Check if it's a notification block.
if(type==TYPE_NOTIFICATION_BLOCK)
addItem(renderer,"Message",_("Message"));
//Add the custom appearance menu item.
addItem(renderer, "Appearance", _("Appearance"), 8 + 4);
addSeparator(renderer);
//Finally add scripting to the bottom.
addItem(renderer,"Scripting",_("Scripting"),8*2+1);
}
void addLevelItems(SDL_Renderer& renderer){
// add the layers
{
// background layers.
auto it = parent->sceneryLayers.begin();
for (; it != parent->sceneryLayers.end(); ++it){
if (it->first >= "f") break; // now we meet a foreground layer
int icon = parent->layerVisibility[it->first] ? (8 * 3 + 1) : (8 * 3 + 2);
icon |= (parent->selectedLayer == it->first ? 3 : 36) << 8;
std::string s = "_layer:" + it->first;
addItem(renderer, s.c_str(), tfm::format(_("Background layer: %s"), it->first).c_str(), icon);
}
// the Blocks layer.
{
int icon = parent->layerVisibility[std::string()] ? (8 * 3 + 1) : (8 * 3 + 2);
icon |= (parent->selectedLayer.empty() ? 3 : 36) << 8;
addItem(renderer, "_layer:", _("Blocks layer"), icon);
}
// foreground layers.
for (; it != parent->sceneryLayers.end(); ++it){
int icon = parent->layerVisibility[it->first] ? (8 * 3 + 1) : (8 * 3 + 2);
icon |= (parent->selectedLayer == it->first ? 3 : 36) << 8;
std::string s = "_layer:" + it->first;
addItem(renderer, s.c_str(), tfm::format(_("Foreground layer: %s"), it->first).c_str(), icon);
}
}
addSeparator(renderer);
addItem(renderer, "AddLayer", _("Add new layer"), 8 * 3 + 6);
addItem(renderer, "DeleteLayer", _("Delete selected layer"), 8 * 3 + 7, parent->selectedLayer.empty());
addItem(renderer, "LayerSettings", _("Configure selected layer"), 8 * 3 + 8, parent->selectedLayer.empty());
addItem(renderer, "MoveToLayer", _("Move selected object to layer"), 0, parent->selectedLayer.empty() || parent->selection.empty());
addSeparator(renderer);
addItem(renderer,"LevelSettings",_("Settings"),8*2);
addItem(renderer,"LevelScripting",_("Scripting"),8*2+1);
}
virtual ~LevelEditorActionsPopup(){
//bmGui is freed by imageManager.
if(actions)
delete actions;
}
void render(SDL_Renderer& renderer){
//Check if we need to resize the separator.
//NOTE: if separatorTexture is NULL then it means that we didn't use the separator at all, so we don't need to update it.
if (separatorTexture && textureWidth(*separatorTexture) < rect.w) {
updateSeparators(renderer);
}
//Draw the actions.
actions->render(renderer,rect.x,rect.y);
}
void handleEvents(SDL_Renderer& renderer){
//Check if a mouse is pressed outside the popup.
int x,y;
SDL_GetMouseState(&x,&y);
SDL_Rect mouse={x,y,0,0};
if(event.type==SDL_MOUSEBUTTONDOWN && !pointOnRect(mouse,rect)){
dismiss();
return;
}
//Let the listbox handle its events.
actions->handleEvents(renderer,rect.x,rect.y);
}
static void addLayerNameNote(ImageManager& imageManager, SDL_Renderer& renderer, GUIWindow *root, int yy = 148) {
std::string s = _("NOTE: the layers are sorted by name alphabetically.\nThe layer is background layer if its name is < 'f'\nby dictionary order, otherwise it's foreground layer.");
for (int lps = 0;;) {
size_t lpe = s.find_first_of('\n', lps);
GUIObject *obj = new GUILabel(imageManager, renderer, 40, yy, 520, 36,
lpe == string::npos ? (s.c_str() + lps) : s.substr(lps, lpe - lps).c_str());
root->addChild(obj);
if (lpe == string::npos) break;
lps = lpe + 1;
yy += 24;
}
}
void GUIEventCallback_OnEvent(ImageManager& imageManager, SDL_Renderer& renderer, std::string name,GUIObject* obj,int eventType){
//Skip next mouse up event since we're clicking a list box and possibly showing a new window.
GUISkipNextMouseUpEvent = true;
//NOTE: There should only be one GUIObject, so we know what event is fired.
//Get the selected entry.
std::string action=actions->item[actions->value];
if(action=="Select"){
//Add the target to the selection.
parent->selection.push_back(target);
dismiss();
return;
}else if(action=="Deselect"){
//Check if the block is in the selection.
std::vector<GameObject*>::iterator it;
it=find(parent->selection.begin(),parent->selection.end(),target);
if(it!=parent->selection.end()){
//Remove the object from the selection.
parent->selection.erase(it);
}
dismiss();
return;
}else if(action=="Delete"){
parent->commandManager->doCommand(new AddRemoveGameObjectCommand(parent, target, false));
dismiss();
return;
}else if(action=="Link"){
parent->linking=true;
parent->linkingTrigger=dynamic_cast<Block*>(target);
parent->tool=LevelEditor::SELECT;
dismiss();
return;
}else if(action=="Remove Links"){
//Remove all the links
Block *block = dynamic_cast<Block*>(target);
if (block) {
RemoveLinkCommand* pCommand = new RemoveLinkCommand(parent, block);
parent->commandManager->doCommand(pCommand);
}
dismiss();
return;
}else if(action=="Path"){
parent->moving=true;
parent->pauseMode = false;
parent->pauseTime = 0;
parent->movingBlock=dynamic_cast<Block*>(target);
parent->tool=LevelEditor::SELECT;
dismiss();
return;
}else if(action=="Remove Path"){
//Remove all the paths
Block *block = dynamic_cast<Block*>(target);
if (block) {
RemovePathCommand* pCommand = new RemovePathCommand(parent, block);
parent->commandManager->doCommand(pCommand);
}
dismiss();
return;
}else if(action=="Message"){
//Create the GUI.
GUIWindow* root=new GUIWindow(imageManager,renderer,(SCREEN_WIDTH-600)/2,(SCREEN_HEIGHT-250)/2,600,250,true,true,_("Notification block"));
root->minWidth = root->width; root->minHeight = root->height;
root->name="notificationBlockWindow";
root->eventCallback=parent;
GUIObject* obj;
obj=new GUILabel(imageManager,renderer,40,50,240,36,_("Enter message here:"));
root->addChild(obj);
GUITextArea* textarea=new GUITextArea(imageManager,renderer,50,90,500,100);
textarea->gravityRight = textarea->gravityBottom = GUIGravityRight;
//Set the name of the text area, which is used to identify the object later on.
textarea->name="message";
string tmp=target->getEditorProperty("message");
//Change \n with the characters '\n'.
while(tmp.find("\\n")!=string::npos){
tmp=tmp.replace(tmp.find("\\n"),2,"\n");
}
textarea->setString(renderer, tmp);
root->addChild(textarea);
obj=new GUIButton(imageManager,renderer,root->width*0.3,250-44,-1,36,_("OK"),0,true,true,GUIGravityCenter);
obj->gravityLeft = obj->gravityRight = GUIGravityCenter;
obj->gravityTop = obj->gravityBottom = GUIGravityRight;
obj->name="cfgNotificationBlockOK";
obj->eventCallback=root;
root->addChild(obj);
obj=new GUIButton(imageManager,renderer,root->width*0.7,250-44,-1,36,_("Cancel"),0,true,true,GUIGravityCenter);
obj->gravityLeft = obj->gravityRight = GUIGravityCenter;
obj->gravityTop = obj->gravityBottom = GUIGravityRight;
obj->name="cfgCancel";
obj->eventCallback=root;
root->addChild(obj);
//Add the window to the GUIObjectRoot and the objectWindows map.
GUIObjectRoot->addChild(root);
parent->objectWindows[root]=target;
//And dismiss this popup.
dismiss();
return;
}else if(action=="Activated"){
//Get the previous state.
bool enabled=(target->getEditorProperty("activated")=="1");
//Switch the state.
enabled=!enabled;
parent->commandManager->doCommand(new SetEditorPropertyCommand(parent, imageManager, renderer,
target, "activated", enabled ? "1" : "0", _("Activated")));
updateItem(renderer,actions->value,"Activated",_("Activated"),enabled?2:1);
actions->value=-1;
return;
} else if (action == "Visible"){
//Get the previous state.
bool visible = (target->getEditorProperty("visible") == "1");
//Switch the state.
visible = !visible;
parent->commandManager->doCommand(new SetEditorPropertyCommand(parent, imageManager, renderer,
target, "visible", visible ? "1" : "0", _("Visible")));
updateItem(renderer, actions->value, "Visible", _("Visible"), visible ? 2 : 1);
actions->value = -1;
return;
} else if (action == "Looping"){
//Get the previous state.
bool loop=(target->getEditorProperty("loop")=="1");
//Switch the state.
loop=!loop;
parent->commandManager->doCommand(new SetEditorPropertyCommand(parent, imageManager, renderer,
target, "loop", loop ? "1" : "0", _("Looping")));
updateItem(renderer,actions->value,"Looping",_("Looping"),loop?2:1);
actions->value=-1;
return;
}else if(action=="Automatic"){
//Get the previous state.
bool automatic=(target->getEditorProperty("automatic")=="1");
//Switch the state.
automatic=!automatic;
parent->commandManager->doCommand(new SetEditorPropertyCommand(parent, imageManager, renderer,
target, "automatic", automatic ? "1" : "0", _("Automatic")));
updateItem(renderer,actions->value,"Automatic",_("Automatic"),automatic?2:1);
actions->value=-1;
return;
}else if(action=="Behaviour"){
//Get the current behaviour.
int currentBehaviour=2;
string behave=target->getEditorProperty("behaviour");
if(behave=="on"){
currentBehaviour=0;
}else if(behave=="off"){
currentBehaviour=1;
}
//Increase the behaviour.
currentBehaviour++;
if(currentBehaviour>2)
currentBehaviour=0;
//Update the data of the block.
parent->commandManager->doCommand(new SetEditorPropertyCommand(parent, imageManager, renderer,
target, "behaviour", behaviour[currentBehaviour], _("Behavior")));
//And update the item.
updateItem(renderer, actions->value, "Behaviour", tfm::format(_("Behavior: %s"), behaviour[currentBehaviour]).c_str());
actions->value=-1;
return;
}else if(action=="State"){
//Get the current state.
int currentState=atoi(target->getEditorProperty("state").c_str());
//Increase the state.
currentState++;
if(currentState>3)
currentState=0;
//Update the data of the block.
char s[64];
sprintf(s,"%d",currentState);
parent->commandManager->doCommand(new SetEditorPropertyCommand(parent, imageManager, renderer,
target, "state", s, _("State")));
//And update the item.
updateItem(renderer, actions->value, "State", tfm::format(_("State: %s"), states[currentState]).c_str());
actions->value=-1;
return;
}else if(action=="Speed"){
//Create the GUI.
GUIWindow* root=new GUIWindow(imageManager,renderer,(SCREEN_WIDTH-600)/2,(SCREEN_HEIGHT-250)/2,600,250,true,true,_("Conveyor belt speed"));
root->minWidth = root->width; root->minHeight = root->height;
root->name="conveyorBlockWindow";
root->eventCallback=parent;
GUIObject* obj;
obj=new GUILabel(imageManager,renderer,40,100,240,36,_("Enter speed here:"));
root->addChild(obj);
GUISpinBox* obj2=new GUISpinBox(imageManager,renderer,240,100,320,36);
obj2->gravityRight = GUIGravityRight;
//Set the name of the text area, which is used to identify the object later on.
obj2->name="speed";
obj2->caption=target->getEditorProperty("speed10");
obj2->format = "%1.0f";
obj2->update();
root->addChild(obj2);
obj = new GUILabel(imageManager, renderer, 40, 160, 520, 36, _("NOTE: 1 Speed = 0.08 block/s"));
root->addChild(obj);
obj=new GUIButton(imageManager,renderer,root->width*0.3,250-44,-1,36,_("OK"),0,true,true,GUIGravityCenter);
obj->gravityLeft = obj->gravityRight = GUIGravityCenter;
obj->gravityTop = obj->gravityBottom = GUIGravityRight;
obj->name="cfgConveyorBlockOK";
obj->eventCallback=root;
root->addChild(obj);
obj=new GUIButton(imageManager,renderer,root->width*0.7,250-44,-1,36,_("Cancel"),0,true,true,GUIGravityCenter);
obj->gravityLeft = obj->gravityRight = GUIGravityCenter;
obj->gravityTop = obj->gravityBottom = GUIGravityRight;
obj->name="cfgCancel";
obj->eventCallback=root;
root->addChild(obj);
//Add the window to the GUIObjectRoot and the objectWindows map.
GUIObjectRoot->addChild(root);
parent->objectWindows[root]=target;
//And dismiss this popup.
dismiss();
return;
}else if(action=="Scripting"){
//Create the GUI.
GUIWindow* root=new GUIWindow(imageManager,renderer,(SCREEN_WIDTH-600)/2,(SCREEN_HEIGHT-500)/2,600,500,true,true,_("Scripting"));
root->minWidth = root->width; root->minHeight = root->height;
root->name="scriptingWindow";
root->eventCallback=parent;
GUIObject* obj;
obj=new GUILabel(imageManager,renderer,50,60,240,36,_("Id:"));
root->addChild(obj);
obj=new GUITextBox(imageManager,renderer,100,60,240,36,dynamic_cast<Block*>(target)->id.c_str());
obj->gravityRight = GUIGravityRight;
obj->name="id";
root->addChild(obj);
GUISingleLineListBox* list=new GUISingleLineListBox(imageManager,renderer,50,100,500,36);
list->gravityRight = GUIGravityRight;
std::map<std::string,int>::iterator it;
for(it=Game::gameObjectEventNameMap.begin();it!=Game::gameObjectEventNameMap.end();++it)
list->addItem(it->first);
list->name="cfgScriptingEventType";
list->value=0;
list->eventCallback=root;
root->addChild(list);
//Add a text area for each event type.
Block* block=dynamic_cast<Block*>(target);
for(unsigned int i=0;i<list->item.size();i++){
GUITextArea* text=new GUITextArea(imageManager,renderer,50,140,500,300);
text->gravityRight = text->gravityBottom = GUIGravityRight;
text->name=list->item[i].first;
text->setFont(fontMono);
//Only set the first one visible and enabled.
text->visible=(i==0);
text->enabled=(i==0);
map<int,string>::iterator it=block->scripts.find(Game::gameObjectEventNameMap[list->item[i].first]);
if(it!=block->scripts.end())
text->setString(renderer, it->second);
root->addChild(text);
}
obj=new GUIButton(imageManager,renderer,root->width*0.3,500-44,-1,36,_("OK"),0,true,true,GUIGravityCenter);
obj->gravityLeft = obj->gravityRight = GUIGravityCenter;
obj->gravityTop = obj->gravityBottom = GUIGravityRight;
obj->name="cfgScriptingOK";
obj->eventCallback=root;
root->addChild(obj);
obj=new GUIButton(imageManager,renderer,root->width*0.7,500-44,-1,36,_("Cancel"),0,true,true,GUIGravityCenter);
obj->gravityLeft = obj->gravityRight = GUIGravityCenter;
obj->gravityTop = obj->gravityBottom = GUIGravityRight;
obj->name="cfgCancel";
obj->eventCallback=root;
root->addChild(obj);
//Add the window to the GUIObjectRoot and the objectWindows map.
GUIObjectRoot->addChild(root);
parent->objectWindows[root]=target;
//And dismiss this popup.
dismiss();
return;
}else if(action=="LevelSettings"){
//Open the levelSettings window.
parent->levelSettings(imageManager,renderer);
//And dismiss this popup.
dismiss();
return;
}else if(action=="LevelScripting"){
//Create the GUI.
GUIWindow* root=new GUIWindow(imageManager,renderer,(SCREEN_WIDTH-600)/2,(SCREEN_HEIGHT-500)/2,600,500,true,true,_("Level Scripting"));
root->minWidth = root->width; root->minHeight = root->height;
root->name="levelScriptingWindow";
root->eventCallback=parent;
GUIObject* obj;
GUISingleLineListBox* list=new GUISingleLineListBox(imageManager,renderer,50,60,500,36);
list->gravityRight = GUIGravityRight;
std::map<std::string,int>::iterator it;
for(it=Game::levelEventNameMap.begin();it!=Game::levelEventNameMap.end();++it)
list->addItem(it->first);
list->name="cfgLevelScriptingEventType";
list->value=0;
list->eventCallback=root;
root->addChild(list);
//Add a text area for each event type.
for(unsigned int i=0;i<list->item.size();i++){
GUITextArea* text=new GUITextArea(imageManager,renderer,50,100,500,340);
text->gravityRight = text->gravityBottom = GUIGravityRight;
text->name=list->item[i].first;
text->setFont(fontMono);
//Only set the first one visible and enabled.
text->visible=(i==0);
text->enabled=(i==0);
map<int,string>::iterator it=parent->scripts.find(Game::levelEventNameMap[list->item[i].first]);
if(it!=parent->scripts.end())
text->setString(renderer, it->second);
root->addChild(text);
}
obj=new GUIButton(imageManager,renderer,root->width*0.3,500-44,-1,36,_("OK"),0,true,true,GUIGravityCenter);
obj->gravityLeft = obj->gravityRight = GUIGravityCenter;
obj->gravityTop = obj->gravityBottom = GUIGravityRight;
obj->name="cfgLevelScriptingOK";
obj->eventCallback=root;
root->addChild(obj);
obj=new GUIButton(imageManager,renderer,root->width*0.7,500-44,-1,36,_("Cancel"),0,true,true,GUIGravityCenter);
obj->gravityLeft = obj->gravityRight = GUIGravityCenter;
obj->gravityTop = obj->gravityBottom = GUIGravityRight;
obj->name="cfgCancel";
obj->eventCallback=root;
root->addChild(obj);
//Add the window to the GUIObjectRoot and the objectWindows map.
GUIObjectRoot->addChild(root);
parent->objectWindows[root]=target;
//And dismiss this popup.
dismiss();
return;
} else if (action.size() >= 7 && action.substr(0, 7) == "_layer:") {
std::string s0 = action.substr(7);
auto it = parent->layerVisibility.find(s0);
if (it != parent->layerVisibility.end()) {
int x;
SDL_GetMouseState(&x, NULL);
if (x < rect.x + 24) {
// toggle the visibility
it->second = !it->second;
if (parent->selectedLayer == it->first) {
// deselect all blocks if the visibility of current layer is changed
parent->deselectAll();
}
} else if (parent->selectedLayer != it->first) {
// deselect all blocks if the selected layer is changed
parent->deselectAll();
// uncheck the previously selected layer
std::string oldSelected = "_layer:" + parent->selectedLayer;
for (unsigned int idx = 0; idx < actions->item.size(); idx++) {
if (actions->item[idx] == oldSelected) {
int icon = parent->layerVisibility[parent->selectedLayer] ? (8 * 3 + 1) : (8 * 3 + 2);
icon |= 36 << 8;
updateItem(renderer, idx, oldSelected.c_str(),
parent->selectedLayer.empty() ? _("Blocks layer") :
tfm::format((parent->selectedLayer < "f") ? _("Background layer: %s") : _("Foreground layer: %s"), parent->selectedLayer).c_str(),
icon);
break;
}
}
// change the selected layer
parent->selectedLayer = it->first;
} else {
actions->value = -1;
return;
}
int icon = it->second ? (8 * 3 + 1) : (8 * 3 + 2);
icon |= (parent->selectedLayer == it->first ? 3 : 36) << 8;
std::string s = "_layer:" + it->first;
updateItem(renderer, actions->value, s.c_str(),
it->first.empty() ? _("Blocks layer") :
tfm::format((it->first < "f") ? _("Background layer: %s") : _("Foreground layer: %s"), it->first).c_str(),
icon);
// update some other menu items according to selection/visibility changes
for (unsigned int i = 0; i < actions->item.size(); i++) {
if (actions->item[i] == "DeleteLayer") {
updateItem(renderer, i, "DeleteLayer", _("Delete selected layer"), 8 * 3 + 7, parent->selectedLayer.empty());
} else if (actions->item[i] == "LayerSettings") {
updateItem(renderer, i, "LayerSettings", _("Configure selected layer"), 8 * 3 + 8, parent->selectedLayer.empty());
} else if (actions->item[i] == "MoveToLayer") {
updateItem(renderer, i, "MoveToLayer", _("Move selected object to layer"), 0, parent->selectedLayer.empty() || parent->selection.empty());
}
}
}
actions->value = -1;
return;
} else if (action == "AddLayer") {
//Create the add layer GUI.
GUIWindow* root = new GUIWindow(imageManager, renderer, (SCREEN_WIDTH - 600) / 2, (SCREEN_HEIGHT - 400) / 2, 600, 400, true, true, _("Add layer"));
root->minWidth = root->width; root->minHeight = root->height;
root->name = "addLayerWindow";
root->eventCallback = parent;
GUIObject* obj;
obj = new GUILabel(imageManager, renderer, 40, 64, 520, 36, _("Enter the layer name:"));
root->addChild(obj);
GUITextBox* obj2 = new GUITextBox(imageManager, renderer, 40, 100, 520, 36);
obj2->gravityRight = GUIGravityRight;
//Set the name of the text area, which is used to identify the object later on.
obj2->name = "layerName";
root->addChild(obj2);
addLayerNameNote(imageManager, renderer, root);
obj = new GUIButton(imageManager, renderer, root->width*0.3, 400 - 44, -1, 36, _("OK"), 0, true, true, GUIGravityCenter);
obj->gravityLeft = obj->gravityRight = GUIGravityCenter;
obj->gravityTop = obj->gravityBottom = GUIGravityRight;
obj->name = "cfgAddLayerOK";
obj->eventCallback = root;
root->addChild(obj);
obj = new GUIButton(imageManager, renderer, root->width*0.7, 400 - 44, -1, 36, _("Cancel"), 0, true, true, GUIGravityCenter);
obj->gravityLeft = obj->gravityRight = GUIGravityCenter;
obj->gravityTop = obj->gravityBottom = GUIGravityRight;
obj->name = "cfgCancel";
obj->eventCallback = root;
root->addChild(obj);
//Add the window to the GUIObjectRoot and the objectWindows map.
GUIObjectRoot->addChild(root);
parent->objectWindows[root] = target;
//And dismiss this popup.
dismiss();
return;
} else if (action == "DeleteLayer") {
// delete selected layer
if (parent->selectedLayer.empty()) {
// can't delete Blocks layer
actions->value = -1;
return;
}
if (parent->sceneryLayers.find(parent->selectedLayer) == parent->sceneryLayers.end()) {
// can't find the layer with given name
actions->value = -1;
return;
}
if (msgBox(imageManager, renderer,
tfm::format(_("Are you sure you want to delete layer '%s'?"), parent->selectedLayer).c_str(),
MsgBoxYesNo, _("Delete layer")) == MsgBoxYes)
{
// do the actual operation
parent->commandManager->doCommand(new AddRemoveLayerCommand(parent, parent->selectedLayer, false));
}
dismiss();
return;
} else if (action == "LayerSettings") {
// rename selected layer
if (parent->selectedLayer.empty()) {
// can't rename Blocks layer
actions->value = -1;
return;
}
auto it = parent->sceneryLayers.find(parent->selectedLayer);
if (it == parent->sceneryLayers.end()) {
// can't find the layer with given name
actions->value = -1;
return;
}
//Create the rename layer GUI.
GUIWindow* root = new GUIWindow(imageManager, renderer, (SCREEN_WIDTH - 600) / 2, (SCREEN_HEIGHT - 500) / 2, 600, 500, true, true, _("Layer settings"));
root->minWidth = root->width; root->minHeight = root->height;
root->name = "layerSettingsWindow";
root->eventCallback = parent;
GUIObject* obj;
obj = new GUILabel(imageManager, renderer, 40, 64, 520, 36, _("Layer name:"));
root->addChild(obj);
GUITextBox* textBox = new GUITextBox(imageManager, renderer, 40, 100, 520, 36, it->first.c_str());
textBox->gravityRight = GUIGravityRight;
//Set the name of the text area, which is used to identify the object later on.
textBox->name = "layerName";
root->addChild(textBox);
// A stupid code to save the old name
obj = new GUIObject(imageManager, renderer, 0, 0, 0, 0, it->first.c_str(), 0, false, false);
obj->name = "oldName";
root->addChild(obj);
addLayerNameNote(imageManager, renderer, root);
obj = new GUILabel(imageManager, renderer, 40, 284, 520, 36, _("Layer moving speed (1 speed = 0.8 block/s):"));
root->addChild(obj);
obj = new GUILabel(imageManager, renderer, 40, 320, 40, 36, "X");
root->addChild(obj);
obj = new GUILabel(imageManager, renderer, 320, 320, 40, 36, "Y");
obj->gravityLeft = obj->gravityRight = GUIGravityCenter;
root->addChild(obj);
+ char s[128];
+
GUISpinBox *spinBox = new GUISpinBox(imageManager, renderer, 80, 320, 200, 36);
spinBox->gravityRight = GUIGravityCenter;
spinBox->name = "speedX";
- spinBox->caption = tfm::format("%g", it->second->speedX);
+ sprintf(s, "%g", it->second->speedX);
+ spinBox->caption = s;
spinBox->format = "%g";
root->addChild(spinBox);
spinBox = new GUISpinBox(imageManager, renderer, 360, 320, 200, 36);
spinBox->gravityLeft = GUIGravityCenter; spinBox->gravityRight = GUIGravityRight;
spinBox->name = "speedY";
- spinBox->caption = tfm::format("%g", it->second->speedY);
+ sprintf(s, "%g", it->second->speedY);
+ spinBox->caption = s;
spinBox->format = "%g";
root->addChild(spinBox);
obj = new GUILabel(imageManager, renderer, 40, 364, 520, 36, _("Speed of following camera:"));
root->addChild(obj);
obj = new GUILabel(imageManager, renderer, 40, 400, 40, 36, "X");
root->addChild(obj);
obj = new GUILabel(imageManager, renderer, 320, 400, 40, 36, "Y");
obj->gravityLeft = obj->gravityRight = GUIGravityCenter;
root->addChild(obj);
spinBox = new GUISpinBox(imageManager, renderer, 80, 400, 200, 36);
spinBox->gravityRight = GUIGravityCenter;
spinBox->name = "cameraX";
- spinBox->caption = tfm::format("%g", it->second->cameraX);
+ sprintf(s, "%g", it->second->cameraX);
+ spinBox->caption = s;
spinBox->format = "%g";
spinBox->change = 0.1f;
root->addChild(spinBox);
spinBox = new GUISpinBox(imageManager, renderer, 360, 400, 200, 36);
spinBox->gravityLeft = GUIGravityCenter; spinBox->gravityRight = GUIGravityRight;
spinBox->name = "cameraY";
- spinBox->caption = tfm::format("%g", it->second->cameraY);
+ sprintf(s, "%g", it->second->cameraY);
+ spinBox->caption = s;
spinBox->format = "%g";
spinBox->change = 0.1f;
root->addChild(spinBox);
obj = new GUIButton(imageManager, renderer, root->width*0.3, 500 - 44, -1, 36, _("OK"), 0, true, true, GUIGravityCenter);
obj->gravityLeft = obj->gravityRight = GUIGravityCenter;
obj->gravityTop = obj->gravityBottom = GUIGravityRight;
obj->name = "cfgLayerSettingsOK";
obj->eventCallback = root;
root->addChild(obj);
obj = new GUIButton(imageManager, renderer, root->width*0.7, 500 - 44, -1, 36, _("Cancel"), 0, true, true, GUIGravityCenter);
obj->gravityLeft = obj->gravityRight = GUIGravityCenter;
obj->gravityTop = obj->gravityBottom = GUIGravityRight;
obj->name = "cfgCancel";
obj->eventCallback = root;
root->addChild(obj);
//Add the window to the GUIObjectRoot and the objectWindows map.
GUIObjectRoot->addChild(root);
parent->objectWindows[root] = target;
//And dismiss this popup.
dismiss();
return;
} else if (action == "MoveToLayer") {
// move the selected object to another layer
if (parent->selection.empty() || parent->selectedLayer.empty()) {
// either nothing selected, or can't move objects in Blocks layer
actions->value = -1;
return;
}
//Create the rename layer GUI.
GUIWindow* root = new GUIWindow(imageManager, renderer, (SCREEN_WIDTH - 600) / 2, (SCREEN_HEIGHT - 300) / 2, 600, 400, true, true, _("Move to layer"));
root->minWidth = root->width; root->minHeight = root->height;
root->name = "moveToLayerWindow";
root->eventCallback = parent;
GUIObject* obj;
obj = new GUILabel(imageManager, renderer, 40, 64, 520, 36, _("Enter the layer name (create new layer if necessary):"));
root->addChild(obj);
GUITextBox* obj2 = new GUITextBox(imageManager, renderer, 40, 100, 520, 36, parent->selectedLayer.c_str());
obj2->gravityRight = GUIGravityRight;
//Set the name of the text area, which is used to identify the object later on.
obj2->name = "layerName";
root->addChild(obj2);
// A stupid code to save the old name
obj = new GUIObject(imageManager, renderer, 0, 0, 0, 0, parent->selectedLayer.c_str(), 0, false, false);
obj->name = "oldName";
root->addChild(obj);
addLayerNameNote(imageManager, renderer, root);
obj = new GUIButton(imageManager, renderer, root->width*0.3, 400 - 44, -1, 36, _("OK"), 0, true, true, GUIGravityCenter);
obj->gravityLeft = obj->gravityRight = GUIGravityCenter;
obj->gravityTop = obj->gravityBottom = GUIGravityRight;
obj->name = "cfgMoveToLayerOK";
obj->eventCallback = root;
root->addChild(obj);
obj = new GUIButton(imageManager, renderer, root->width*0.7, 400 - 44, -1, 36, _("Cancel"), 0, true, true, GUIGravityCenter);
obj->gravityLeft = obj->gravityRight = GUIGravityCenter;
obj->gravityTop = obj->gravityBottom = GUIGravityRight;
obj->name = "cfgCancel";
obj->eventCallback = root;
root->addChild(obj);
//Add the window to the GUIObjectRoot and the objectWindows map.
GUIObjectRoot->addChild(root);
parent->objectWindows[root] = target;
//And dismiss this popup.
dismiss();
return;
} else if (action.size() > 10 && action.substr(0, 10) == "RepeatMode") {
Scenery *scenery = dynamic_cast<Scenery*>(target);
if (scenery) {
int index = atoi(action.c_str() + 10);
assert(index >= 0 && index < 4);
//Get the current repeat mode.
unsigned int repeatMode = scenery->repeatMode;
//Extract the value we want to modify.
unsigned int i = (repeatMode >> (index * 8)) & 0xFF;
repeatMode &= ~(0xFFu << (index * 8));
//Increase the value.
for (;;) {
i++;
if (i >= Scenery::REPEAT_MODE_MAX) i = 0;
// skip invalid values (POSITIVE_INFINITY for start, NEGATIVE_INFINITY for end)
if ((index & 1) == 0 && i == Scenery::POSITIVE_INFINITY) continue;
if ((index & 1) == 1 && i == Scenery::NEGATIVE_INFINITY) continue;
break;
}
//Update the repeat mode of the block.
char s[64];
sprintf(s, "%u", repeatMode | (i << (index * 8)));
parent->commandManager->doCommand(new SetEditorPropertyCommand(parent, imageManager, renderer,
target, "repeatMode", s, _("Repeat mode")));
//And update the item.
const char* ss[4] = {
__("Horizontal repeat start: %s"),
__("Horizontal repeat end: %s"),
__("Vertical repeat start: %s"),
__("Vertical repeat end: %s"),
};
const int icons[4] = {
8 * 2 + 3, 8 * 2 + 4, 8 * 3 + 3, 8 * 3 + 4,
};
updateItem(renderer, actions->value, action.c_str(), tfm::format(_(ss[index]), getRepeatModeName(i)).c_str(), icons[index]);
}
actions->value = -1;
return;
} else if (action == "CustomScenery") {
//Create the GUI.
GUIWindow* root = new GUIWindow(imageManager, renderer, (SCREEN_WIDTH - 600) / 2, (SCREEN_HEIGHT - 400) / 2, 600, 400, true, true, _("Custom scenery"));
root->minWidth = root->width; root->minHeight = root->height;
root->name = "customSceneryWindow";
root->eventCallback = parent;
GUIObject* obj;
obj = new GUILabel(imageManager, renderer, 50, 60, 240, 36, _("Custom scenery:"));
root->addChild(obj);
//Add a text area.
Scenery* scenery = dynamic_cast<Scenery*>(target);
GUITextArea* text = new GUITextArea(imageManager, renderer, 50, 100, 500, 240);
text->gravityRight = text->gravityBottom = GUIGravityRight;
text->name = "cfgCustomScenery";
text->setFont(fontMono);
//Only set the first one visible and enabled.
text->visible = true;
text->enabled = true;
// FIXME: an ad-hoc code
std::string s;
for (char c : scenery->customScenery_) {
if (c == '\t')
s.append(" ");
else
s.push_back(c);
}
text->setString(renderer, s);
root->addChild(text);
obj = new GUIButton(imageManager, renderer, root->width*0.3, 400 - 44, -1, 36, _("OK"), 0, true, true, GUIGravityCenter);
obj->gravityLeft = obj->gravityRight = GUIGravityCenter;
obj->gravityTop = obj->gravityBottom = GUIGravityRight;
obj->name = "cfgCustomSceneryOK";
obj->eventCallback = root;
root->addChild(obj);
obj = new GUIButton(imageManager, renderer, root->width*0.7, 400 - 44, -1, 36, _("Cancel"), 0, true, true, GUIGravityCenter);
obj->gravityLeft = obj->gravityRight = GUIGravityCenter;
obj->gravityTop = obj->gravityBottom = GUIGravityRight;
obj->name = "cfgCancel";
obj->eventCallback = root;
root->addChild(obj);
//Add the window to the GUIObjectRoot and the objectWindows map.
GUIObjectRoot->addChild(root);
parent->objectWindows[root] = target;
//And dismiss this popup.
dismiss();
return;
} else if (action == "Appearance"){
//Create the GUI.
GUIWindow* root = new GUIWindow(imageManager, renderer, (SCREEN_WIDTH - 600) / 2, (SCREEN_HEIGHT - 400) / 2, 600, 400, true, true, _("Appearance"));
root->minWidth = root->width; root->minHeight = root->height;
root->name = "appearanceWindow";
root->eventCallback = parent;
GUIObject* obj;
Block* block = dynamic_cast<Block*>(target);
//Create a list box showing all available scenery blocks.
GUIListBox *list = new GUIListBox(imageManager, renderer, 50, 60, 440, 280);
list->gravityRight = list->gravityBottom = GUIGravityRight;
list->name = "lstAppearance";
list->eventCallback = root;
root->addChild(list);
//TODO: Show the image along with the text in the list box.
//Currently I don't like to do that because this requires a lot of video memory since there are a lot of available scenery blocks.
list->addItem(renderer, _("(Use the default appearance for this block)"));
if (block->customAppearanceName.empty()) list->value = 0;
for (const std::string& s : parent->sceneryBlockNames) {
list->addItem(renderer, describeSceneryName(s));
if (block->customAppearanceName == s) list->value = list->item.size() - 1;
}
if (list->value < 0) {
std::cerr << "WARNING: The custom appearance '" << block->customAppearanceName << "' is unrecognized, added it to the list box anyway" << std::endl;
list->addItem(renderer, block->customAppearanceName);
list->value = list->item.size() - 1;
}
//Ask the list box to update scrollbar and we scroll the scrollbar to the correct position (FIXME: ad-hoc code)
list->render(renderer, 0, 0, false);
list->scrollScrollbar(list->value);
//Create an image widget showing the appearance of selected scenery block.
GUIImage *image = new GUIImage(imageManager, renderer, 500, 60, 50, 50);
image->gravityLeft = image->gravityRight = GUIGravityRight;
image->name = "imgAppearance";
root->addChild(image);
//Add a Change event to the list box which will be processed next frame, which is used to update the image widget.
GUIEventQueue.push_back(GUIEvent{ list->eventCallback, list->name, list, GUIEventChange });
obj = new GUIButton(imageManager, renderer, root->width*0.3, 400 - 44, -1, 36, _("OK"), 0, true, true, GUIGravityCenter);
obj->gravityLeft = obj->gravityRight = GUIGravityCenter;
obj->gravityTop = obj->gravityBottom = GUIGravityRight;
obj->name = "cfgAppearanceOK";
obj->eventCallback = root;
root->addChild(obj);
obj = new GUIButton(imageManager, renderer, root->width*0.7, 400 - 44, -1, 36, _("Cancel"), 0, true, true, GUIGravityCenter);
obj->gravityLeft = obj->gravityRight = GUIGravityCenter;
obj->gravityTop = obj->gravityBottom = GUIGravityRight;
obj->name = "cfgCancel";
obj->eventCallback = root;
root->addChild(obj);
//Add the window to the GUIObjectRoot and the objectWindows map.
GUIObjectRoot->addChild(root);
parent->objectWindows[root] = target;
//And dismiss this popup.
dismiss();
return;
}
}
};
/////////////////LevelEditorSelectionPopup/////////////////
class LevelEditorSelectionPopup{
private:
//The parent object.
LevelEditor* parent;
//The position of window.
SDL_Rect rect;
//GUI image.
SharedTexture bmGUI;
//The selection
std::vector<GameObject*> selection;
//The scrollbar.
GUIScrollBar* scrollBar;
//Highlighted object.
GameObject* highlightedObj;
//Highlighted button index. 0=none 1=select/deselect 2=delete 3=configure
int highlightedBtn;
public:
int startRow,showedRow;
//If selection is dirty
bool dirty;
public:
SDL_Rect getRect(){
return rect;
}
int width(){
return rect.w;
}
int height(){
return rect.h;
}
void updateScrollBar(ImageManager& imageManager, SDL_Renderer& renderer){
int m=selection.size()-showedRow;
if(m>0){
if(startRow<0) startRow=0;
else if(startRow>m) startRow=m;
if(scrollBar==NULL){
scrollBar=new GUIScrollBar(imageManager,renderer,0,0,16,rect.h-16,ScrollBarVertical,startRow,0,m,1,showedRow);
}
scrollBar->visible=true;
scrollBar->maxValue=m;
scrollBar->value=startRow;
}else{
startRow=0;
if(scrollBar){
scrollBar->visible=false;
scrollBar->value=0;
}
}
}
void updateSelection(ImageManager& imageManager, SDL_Renderer& renderer){
if(parent!=NULL){
std::vector<Block*>& v=parent->levelObjects;
for(int i=selection.size()-1;i>=0;i--){
if(find(v.begin(),v.end(),selection[i])==v.end()){
selection.erase(selection.begin()+i);
}
}
updateScrollBar(imageManager,renderer);
}
}
void dismiss(){
if(parent!=NULL && parent->selectionPopup==this){
parent->selectionPopup=NULL;
}
delete this;
}
LevelEditorSelectionPopup(LevelEditor* parent, ImageManager& imageManager, SDL_Renderer& renderer, std::vector<GameObject*>& selection, int x=0, int y=0){
this->parent=parent;
this->selection=selection;
dirty=false;
scrollBar=NULL;
highlightedObj=NULL;
highlightedBtn=0;
//calc window size
startRow=0;
showedRow=selection.size();
int m=SCREEN_HEIGHT/64-1;
if(showedRow>m) showedRow=m;
rect.w=320;
rect.h=showedRow*64+16;
if(x>SCREEN_WIDTH-rect.w) x=SCREEN_WIDTH-rect.w;
else if(x<0) x=0;
if(y>SCREEN_HEIGHT-rect.h) y=SCREEN_HEIGHT-rect.h;
else if(y<0) y=0;
rect.x=x;
rect.y=y;
updateScrollBar(imageManager,renderer);
//Load the gui images.
bmGUI=imageManager.loadTexture(getDataPath()+"gfx/gui.png",renderer);
}
virtual ~LevelEditorSelectionPopup(){
if(scrollBar)
delete scrollBar;
}
void move(int x,int y){
if(x>SCREEN_WIDTH-rect.w) x=SCREEN_WIDTH-rect.w;
else if(x<0) x=0;
if(y>SCREEN_HEIGHT-rect.h) y=SCREEN_HEIGHT-rect.h;
else if(y<0) y=0;
rect.x=x;
rect.y=y;
}
void render(ImageManager& imageManager, SDL_Renderer& renderer){
//Check dirty
if(dirty){
updateSelection(imageManager,renderer);
if(selection.empty()){
dismiss();
return;
}
dirty=false;
}
//background
drawGUIBox(rect.x,rect.y,rect.w,rect.h,renderer,0xFFFFFFFFU);
//get mouse position
int x,y;
SDL_GetMouseState(&x,&y);
SDL_Rect mouse={x,y,0,0};
//the tool tip of item
SDL_Rect tooltipRect;
//string tooltip;
if(scrollBar && scrollBar->visible){
startRow=scrollBar->value;
}
highlightedObj=NULL;
highlightedBtn=0;
ToolTips toolTip = ToolTips::TooltipMax;
int maxWidth = 0;
//draw avaliable item
for(int i=0;i<showedRow;i++){
int j=startRow+i;
if(j>=(int)selection.size()) break;
SDL_Rect r={rect.x+8,rect.y+i*64+8,rect.w-16,64};
if(scrollBar && scrollBar->visible) r.w-=24;
//check highlight
if(pointOnRect(mouse,r)){
highlightedObj=selection[j];
//0xCCCCCC
SDL_SetRenderDrawColor(&renderer,0xCC,0xCC,0xCC,0xFF);
SDL_RenderFillRect(&renderer,&r);
}
const int type = selection[j]->type;
Scenery *scenery = dynamic_cast<Scenery*>(selection[j]);
if (scenery) {
if (scenery->themeBlock == &(scenery->internalThemeBlock)) {
// custom scenery, draw an ad-hoc stupid icon
if (parent) {
const SDL_Rect srcRect = { 48, 16, 16, 16 };
const SDL_Rect dstRect = { r.x + 7, r.y + 7, 16, 16 };
SDL_RenderCopy(&renderer, parent->bmGUI.get(), &srcRect, &dstRect);
}
} else {
scenery->themeBlock->editorPicture.draw(renderer, r.x + 7, r.y + 7);
}
} else {
//draw tile picture
ThemeBlock* obj = objThemes.getBlock(type);
if (obj){
//obj->editorPicture.draw(screen,r.x+7,r.y+7);
obj->editorPicture.draw(renderer, r.x + 7, r.y + 7);
}
}
if(parent!=NULL){
//draw name
TexturePtr& tex = scenery ? (parent->getCachedTextTexture(renderer, scenery->sceneryName_.empty()
/// TRANSLATORS: Block name
? std::string(_("Custom scenery block")) : describeSceneryName(scenery->sceneryName_)))
: parent->typeTextTextures.at(type);
if (tex) {
const int w = textureWidth(tex) + 160;
if (w > maxWidth) maxWidth = w;
applyTexture(r.x + 64, r.y + (64 - textureHeight(tex)) / 2, tex, renderer);
}
//draw selected
{
std::vector<GameObject*> &v=parent->selection;
bool isSelected=find(v.begin(),v.end(),selection[j])!=v.end();
SDL_Rect r1={isSelected?16:0,0,16,16};
SDL_Rect r2={r.x+r.w-72,r.y+20,24,24};
if(pointOnRect(mouse,r2)){
drawGUIBox(r2.x,r2.y,r2.w,r2.h,renderer,0x999999FFU);
tooltipRect=r2;
//tooltip=_("Select");
highlightedBtn=1;
toolTip=ToolTips::Select_UsedInSelectionPopup;
}
r2.x+=4;
r2.y+=4;
r2.w=r1.w;
r2.h=r1.h;
SDL_RenderCopy(&renderer, bmGUI.get(),&r1,&r2);
}
//draw delete
{
SDL_Rect r1={112,0,16,16};
SDL_Rect r2={r.x+r.w-48,r.y+20,24,24};
if(pointOnRect(mouse,r2)){
drawGUIBox(r2.x,r2.y,r2.w,r2.h,renderer,0x999999FFU);
tooltipRect=r2;
//tooltip=_("Delete");
highlightedBtn=2;
toolTip=ToolTips::Delete_UsedInSelectionPopup;
}
r2.x+=4;
r2.y+=4;
r2.w=r1.w;
r2.h=r1.h;
SDL_RenderCopy(&renderer, bmGUI.get(),&r1,&r2);
}
//draw configure
{
SDL_Rect r1={112,16,16,16};
SDL_Rect r2={r.x+r.w-24,r.y+20,24,24};
if(pointOnRect(mouse,r2)){
drawGUIBox(r2.x,r2.y,r2.w,r2.h,renderer,0x999999FFU);
tooltipRect=r2;
//tooltip=_("Configure");
toolTip=ToolTips::Configure_UsedInSelectionPopup;
highlightedBtn=3;
}
r2.x+=4;
r2.y+=4;
r2.w=r1.w;
r2.h=r1.h;
SDL_RenderCopy(&renderer, bmGUI.get(),&r1,&r2);
}
}
}
//draw scrollbar
if(scrollBar && scrollBar->visible){
scrollBar->render(renderer,rect.x+rect.w-24,rect.y+8);
}
//draw tooltip
if(parent && int(toolTip) < parent->tooltipTextures.size()){
//Tool specific text.
TexturePtr& tip=parent->tooltipTextures.at(size_t(toolTip));
//Draw only if there's a tooltip available
if(tip){
const auto tipSize = rectFromTexture(tip);
tooltipRect.y-=4;
tooltipRect.h+=8;
if(tooltipRect.y+tooltipRect.h+tipSize.h>SCREEN_HEIGHT-20)
tooltipRect.y-=tipSize.h;
else
tooltipRect.y+=tooltipRect.h;
if(tooltipRect.x+tipSize.w>SCREEN_WIDTH-20)
tooltipRect.x=SCREEN_WIDTH-20-tipSize.w;
//Draw borders around text
Uint32 color=0xFFFFFF00|230;
drawGUIBox(tooltipRect.x-2,tooltipRect.y-2,tipSize.w+4,tipSize.h+4,renderer,color);
//Draw tooltip's text
applyTexture(tooltipRect.x,tooltipRect.y,tip,renderer);
}
}
//Resize the selection popup if necessary
if (maxWidth > rect.w) {
rect.w = maxWidth;
move(rect.x, rect.y);
}
}
void handleEvents(ImageManager& imageManager,SDL_Renderer& renderer){
//Check dirty
if(dirty){
updateSelection(imageManager,renderer);
if(selection.empty()){
dismiss();
return;
}
dirty=false;
}
//Check scrollbar event
if(scrollBar && scrollBar->visible){
if(scrollBar->handleEvents(renderer,rect.x+rect.w-24,rect.y+8)) return;
}
if(event.type==SDL_MOUSEBUTTONDOWN){
if(event.button.button==SDL_BUTTON_LEFT){
SDL_Rect mouse={event.button.x,event.button.y,0,0};
//Check if close it
if(!pointOnRect(mouse,rect)){
dismiss();
return;
}
//Check if item is clicked
if(highlightedObj!=NULL && highlightedBtn>0 && parent!=NULL){
//std::vector<Block*>& v=parent->levelObjects;
if(/*find(v.begin(),v.end(),highlightedObj)!=v.end()*/true/*???*/){
switch(highlightedBtn){
case 1:
{
std::vector<GameObject*>& v2=parent->selection;
std::vector<GameObject*>::iterator it=find(v2.begin(),v2.end(),highlightedObj);
if(it==v2.end()){
v2.push_back(highlightedObj);
}else{
v2.erase(it);
}
}
break;
case 2:
parent->commandManager->doCommand(new AddRemoveGameObjectCommand(parent, highlightedObj, false));
break;
case 3:
if(parent->actionsPopup)
delete parent->actionsPopup;
parent->actionsPopup=new LevelEditorActionsPopup(imageManager,renderer,parent,highlightedObj,mouse.x,mouse.y);
break;
}
}
}
}
}
else if(event.type == SDL_MOUSEWHEEL) {
//check mousewheel
if(event.wheel.y < 0){
startRow-=2;
updateScrollBar(imageManager,renderer);
return;
} else {
startRow+=2;
updateScrollBar(imageManager,renderer);
return;
}
}
}
};
/////////////////MovingPosition////////////////////////////
MovingPosition::MovingPosition(int x,int y,int time){
this->x=x;
this->y=y;
this->time=time;
}
MovingPosition::~MovingPosition(){}
void MovingPosition::updatePosition(int x,int y){
this->x=x;
this->y=y;
}
/////////////////LEVEL EDITOR//////////////////////////////
LevelEditor::LevelEditor(SDL_Renderer& renderer, ImageManager& imageManager):Game(renderer, imageManager){
//This will set some default settings.
reset();
//Create the GUI root.
GUIObjectRoot=new GUIObject(imageManager,renderer,0,0,SCREEN_WIDTH,SCREEN_HEIGHT);
//Load the toolbar.
toolbar=imageManager.loadTexture(getDataPath()+"gfx/menu/toolbar.png",renderer);
toolbarRect={(SCREEN_WIDTH-460)/2,SCREEN_HEIGHT-50,460,50};
selectionPopup=NULL;
actionsPopup=NULL;
movingSpeedWidth=-1;
//Load the selectionMark.
selectionMark=imageManager.loadTexture(getDataPath()+"gfx/menu/selection.png",renderer);
//Load the movingMark.
movingMark=imageManager.loadTexture(getDataPath()+"gfx/menu/moving.png",renderer);
//Load the gui images.
bmGUI=imageManager.loadTexture(getDataPath()+"gfx/gui.png",renderer);
toolboxText=textureFromText(renderer,*fontText,_("Toolbox"),objThemes.getTextColor(true));
for(size_t i = 0;i < typeTextTextures.size();++i) {
typeTextTextures[i] =
textureFromText(renderer,
*fontText,
_(blockNames[i]),
objThemes.getTextColor(true));
}
for(size_t i = 0;i < tooltipTextures.size();++i) {
if (tooltipNames[i][0]) {
std::string s = _(tooltipNames[i]);
if (tooltipHotkey[i][0]) {
s += " (" + std::string(tooltipHotkey[i]) + ")";
} else if (tooltipHotkey2[i] >= 0) {
std::string s2 = InputManagerKeyCode::describeTwo(
inputMgr.getKeyCode((InputManagerKeys)tooltipHotkey2[i], false),
inputMgr.getKeyCode((InputManagerKeys)tooltipHotkey2[i], true));
if (!s2.empty()) s += " (" + s2 + ")";
}
tooltipTextures[i] = textureFromText(renderer, *fontText, s.c_str(), objThemes.getTextColor(true));
}
}
//Count the level editing time.
statsMgr.startLevelEdit();
//Create the command manager with a maximum of 100 commands.
commandManager = new CommandManager(100);
}
// FIXME: I have to write this function here since we need to access the static blockNames[]
std::string MoveGameObjectCommand::describe() {
if (objects.size() == 1) {
const bool isResize = oldPosition[0].w != newPosition[0].w || oldPosition[0].h != newPosition[0].h;
Scenery *scenery = dynamic_cast<Scenery*>(objects[0]);
return tfm::format(isResize ? _("Resize %s") : _("Move %s"), scenery ?
/// TRANSLATORS: Context: Resize/Move ...
(scenery->sceneryName_.empty() ? _("Custom scenery block")
: describeSceneryName(scenery->sceneryName_).c_str())
: _(blockNames[objects[0]->type]));
} else {
const size_t number_of_objects = objects.size();
/// TRANSLATORS: Context: Undo/Redo ...
return tfm::format(ngettext("Move %d object", "Move %d objects", number_of_objects).c_str(), number_of_objects);
}
}
// FIXME: I have to write this function here since we need to access the static blockNames[]
std::string AddRemoveGameObjectCommand::describe() {
if (objects.size() == 1) {
Scenery *scenery = dynamic_cast<Scenery*>(objects[0]);
return tfm::format(isAdd ? _("Add %s") : _("Remove %s"), scenery ? (scenery->sceneryName_.empty() ?
/// TRANSLATORS: Context: Add/Remove ...
_("Custom scenery block")
: describeSceneryName(scenery->sceneryName_).c_str())
: _(blockNames[objects[0]->type]));
} else {
const size_t number_of_objects = objects.size();
/// TRANSLATORS: Context: Undo/Redo ...
return tfm::format(isAdd ? ngettext("Add %d object", "Add %d objects", number_of_objects).c_str() :
/// TRANSLATORS: Context: Undo/Redo ...
ngettext("Remove %d object", "Remove %d objects", number_of_objects).c_str(), number_of_objects);
}
}
// FIXME: I have to write this function here since we need to access the static blockNames[]
std::string AddRemovePathCommand::describe() {
return tfm::format(isAdd ?
/// TRANSLATORS: Context: Undo/Redo ...
_("Add path to %s") :
/// TRANSLATORS: Context: Undo/Redo ...
_("Remove a path point from %s"), _(blockNames[target->type]));
}
// FIXME: I have to write this function here since we need to access the static blockNames[]
std::string RemovePathCommand::describe() {
/// TRANSLATORS: Context: Undo/Redo ...
return tfm::format(_("Remove all paths from %s"), _(blockNames[target->type]));
}
// FIXME: I have to write this function here since we need to access the static blockNames[]
std::string AddLinkCommand::describe() {
/// TRANSLATORS: Context: Undo/Redo ...
return tfm::format(_("Add link from %s to %s"), _(blockNames[target->type]), _(blockNames[clickedObj->type]));
}
// FIXME: I have to write this function here since we need to access the static blockNames[]
std::string RemoveLinkCommand::describe() {
/// TRANSLATORS: Context: Undo/Redo ...
return tfm::format(_("Remove all links from %s"), _(blockNames[target->type]));
}
// FIXME: I have to write this function here since we need to access the static blockNames[]
std::string SetEditorPropertyCommand::describe() {
Scenery *scenery = dynamic_cast<Scenery*>(target);
/// TRANSLATORS: Context: Undo/Redo ...
std::string s = _("Modify the %2 property of %1");
size_t lp = s.find("%1");
if (lp != string::npos) {
std::string s1 = scenery ?
(scenery->sceneryName_.empty() ?
/// TRANSLATORS: Context: Undo/Redo ...
_("Custom scenery block")
: describeSceneryName(scenery->sceneryName_).c_str())
: _(blockNames[target->type]);
s = s.substr(0, lp) + s1 + s.substr(lp + 2);
}
lp = s.find("%2");
if (lp != string::npos) {
s = s.substr(0, lp) + desc + s.substr(lp + 2);
}
return s;
}
// FIXME: I have to write this function here since we need to access the static variable levelTime and levelRecordings
SetLevelPropertyCommand::SetLevelPropertyCommand(LevelEditor* levelEditor, const LevelProperty& levelProperty)
: editor(levelEditor), newProperty(levelProperty)
{
oldProperty.levelName = editor->levelName;
oldProperty.levelTheme = editor->levelTheme;
oldProperty.levelMusic = editor->levelMusic;
oldProperty.levelTime = levelTime;
oldProperty.levelRecordings = levelRecordings;
}
// FIXME: I have to write this function here since we need to access the static variable levelTime and levelRecordings
void SetLevelPropertyCommand::setLevelProperty(const LevelProperty& levelProperty) {
bool musicChanged = editor->levelMusic != levelProperty.levelMusic;
editor->levelName = levelProperty.levelName;
editor->levelTheme = levelProperty.levelTheme;
editor->levelMusic = levelProperty.levelMusic;
levelTime = levelProperty.levelTime;
levelRecordings = levelProperty.levelRecordings;
if (musicChanged) {
#ifdef _DEBUG
printf("DEBUG: Level music is changed dynamically in level editor\n");
#endif
editor->editorData["music"] = editor->levelMusic;
editor->reloadMusic();
}
}
// FIXME: I have to write this function here since we need to access the static blockNames[]
std::string SetScriptCommand::describe() {
if (target) {
/// TRANSLATORS: Context: Undo/Redo ...
return tfm::format(_("Edit the script of %s"), _(blockNames[target->type]));
} else {
/// TRANSLATORS: Context: Undo/Redo ...
return _("Edit the script of level");
}
}
LevelEditor::~LevelEditor(){
//Delete the command manager.
delete commandManager;
// NOTE: We don't need to delete levelObjects, etc. since they are deleted in Game::~Game().
// Clear selection
selection.clear();
//Delete the GUI.
if(GUIObjectRoot){
delete GUIObjectRoot;
GUIObjectRoot=NULL;
}
//Delete the popup
if(selectionPopup){
delete selectionPopup;
selectionPopup=NULL;
}
//Delete the popup
if(actionsPopup){
delete actionsPopup;
actionsPopup=NULL;
}
//Reset the camera.
camera.x=0;
camera.y=0;
//Count the level editing time.
statsMgr.endLevelEdit();
}
TexturePtr& LevelEditor::getCachedTextTexture(SDL_Renderer& renderer, const std::string& text) {
auto it = cachedTextTextures.find(text);
if (it != cachedTextTextures.end()) return it->second;
return (cachedTextTextures[text] = textureFromText(renderer,
*fontText,
text.c_str(),
objThemes.getTextColor(true)));
}
void LevelEditor::reset(){
//Set some default values.
playMode=false;
tool=ADD;
currentType=0;
toolboxVisible=false;
toolboxRect.x=-1;
toolboxRect.y=-1;
toolboxRect.w=0;
toolboxRect.h=0;
toolboxIndex=0;
pressedShift=false;
pressedLeftMouse=false;
dragging=false;
selectionDrag=-1;
dragCenter=NULL;
cameraXvel=0;
cameraYvel=0;
linking=false;
linkingTrigger=NULL;
currentId=0;
movingBlock=NULL;
moving=false;
movingSpeed=10;
pauseTime = 0;
tooltip=-1;
//Set the player and shadow to their starting position.
player.setLocation(player.fx,player.fy);
shadow.setLocation(shadow.fx,shadow.fy);
selection.clear();
clipboard.clear();
triggers.clear();
movingBlocks.clear();
//Delete any gui.
if(GUIObjectRoot){
delete GUIObjectRoot;
GUIObjectRoot=NULL;
}
//Clear the GUIWindow object map.
objectWindows.clear();
}
void LevelEditor::loadLevelFromNode(ImageManager& imageManager, SDL_Renderer& renderer,TreeStorageNode* obj, const std::string& fileName){
//call the method of base class.
Game::loadLevelFromNode(imageManager,renderer,obj,fileName);
//now do our own stuff.
string s=editorData["time"];
if(s.empty() || !(s[0]>='0' && s[0]<='9')){
levelTime=-1;
}else{
levelTime=atoi(s.c_str());
}
s=editorData["recordings"];
if(s.empty() || !(s[0]>='0' && s[0]<='9')){
levelRecordings=-1;
}else{
levelRecordings=atoi(s.c_str());
}
levelTheme = editorData["theme"];
levelMusic = editorData["music"];
//NOTE: We set the camera here since we know the dimensions of the level.
if(LEVEL_WIDTH<SCREEN_WIDTH)
camera.x=-(SCREEN_WIDTH-LEVEL_WIDTH)/2;
else
camera.x=0;
if(LEVEL_HEIGHT<SCREEN_HEIGHT)
camera.y=-(SCREEN_HEIGHT-LEVEL_HEIGHT)/2;
else
camera.y=0;
//The level is loaded, so call postLoad.
postLoad();
}
void LevelEditor::saveLevel(string fileName){
//Create the output stream and check if it starts.
std::ofstream save(fileName.c_str());
if(!save) return;
//The current level.
LevelPack::Level *currentLevel = levels->getLevel(), *currentLevel2 = NULL;
//Check if the current level is individual level,
//in this case the level are both in "Levels" and "Custom Levels" level packs.
if (levels->type == COLLECTION) {
assert(levels->levelpackPath == CUSTOM_LEVELS_PATH);
if (auto levels2 = getLevelPackManager()->getLevelPack(LEVELS_PATH)) {
for (int i = 0, m = levels2->getLevelCount(); i < m; i++) {
if (levels2->getLevel(i)->file == currentLevel->file) {
currentLevel2 = levels2->getLevel(i);
break;
}
}
if (currentLevel2 == NULL) {
fprintf(stderr, "BUG: The custom level '%s' is not conatined in 'Levels' pack!\n", currentLevel->file.c_str());
}
}
}
//The storageNode to put the level data in before writing it away.
TreeStorageNode node;
char s[64];
//The name of the level.
if(!levelName.empty()){
node.attributes["name"].push_back(levelName);
//Update the level name in the levelpack.
currentLevel->name = levelName;
if (currentLevel2) currentLevel2->name = levelName;
}
//The level theme.
if(!levelTheme.empty())
node.attributes["theme"].push_back(levelTheme);
//The level music.
if (!levelMusic.empty())
node.attributes["music"].push_back(levelMusic);
//target time and recordings.
{
char c[32];
sprintf(c, "%d", std::max(levelTime, -1));
node.attributes["time"].push_back(c);
//Update the target time the levelpack.
currentLevel->targetTime = std::max(levelTime, -1);
if (currentLevel2) currentLevel2->targetTime = std::max(levelTime, -1);
sprintf(c, "%d", std::max(levelRecordings, -1));
node.attributes["recordings"].push_back(c);
//Update the target recordings the levelpack.
currentLevel->targetRecordings = std::max(levelRecordings, -1);
if (currentLevel2) currentLevel2->targetRecordings = std::max(levelRecordings, -1);
}
//The width of the level.
sprintf(s, "%d", LEVEL_WIDTH);
node.attributes["size"].push_back(s);
//The height of the level.
sprintf(s, "%d", LEVEL_HEIGHT);
node.attributes["size"].push_back(s);
//Loop through the gameObjects and save them.
for(int o=0;o<(signed)levelObjects.size();o++){
int objectType=levelObjects[o]->type;
//Check if it's a legal gameObject type.
if(objectType>=0 && objectType<TYPE_MAX){
TreeStorageNode* obj1=new TreeStorageNode;
node.subNodes.push_back(obj1);
//It's a tile so name the node tile.
obj1->name="tile";
//Write away the type of the gameObject.
obj1->value.push_back(blockName[objectType]);
//Get the box for the location of the gameObject.
SDL_Rect box=levelObjects[o]->getBox(BoxType_Base);
//Put the location and size in the storageNode.
sprintf(s,"%d",box.x);
obj1->value.push_back(s);
sprintf(s,"%d",box.y);
obj1->value.push_back(s);
sprintf(s,"%d",box.w);
obj1->value.push_back(s);
sprintf(s,"%d",box.h);
obj1->value.push_back(s);
//Loop through the editor data and save it also.
vector<pair<string,string> > obj;
levelObjects[o]->getEditorData(obj);
for (const auto& o : obj) {
//Skip the data whose key or value is empty.
if (o.first.empty()) continue;
if (o.second.empty()) continue;
//Skip SOME data whose value is the default value. Currently only some boolean values are skipped.
//WARNING: When the default values are changed, these codes MUST be modified accordingly!!!
//NOTE: Currently we skip the "visible" property since it is used for every block and usually it's the default value.
if (o.first == "visible" && o.second == "1") continue;
#if 0
//NOTE: The following codes are more aggressive!!!
//if (o.first == "activated" && o.second == "1") continue; //moving blocks and conveyor belt // Don't use this because there was a "disabled" property
if (o.first == "loop" && o.second == "1") continue; //moving blocks and conveyor belt
if (o.first == "automatic" && o.second == "0") continue; //portal
#endif
//Save the data.
obj1->attributes[o.first].push_back(o.second);
}
//Loop through the scripts and add them to the storage node of the game object.
map<int,string>::iterator it;
Block* object=(dynamic_cast<Block*>(levelObjects[o]));
for(it=object->scripts.begin();it!=object->scripts.end();++it){
//Make sure the script isn't an empty string.
if(it->second.empty())
continue;
TreeStorageNode* script=new TreeStorageNode;
obj1->subNodes.push_back(script);
script->name="script";
script->value.push_back(gameObjectEventTypeMap[it->first]);
script->attributes["script"].push_back(it->second);
}
}
}
//Loop through the level scripts and save them.
for(auto it=scripts.begin();it!=scripts.end();++it){
//Make sure the script isn't an empty string.
if(it->second.empty())
continue;
TreeStorageNode* script=new TreeStorageNode;
node.subNodes.push_back(script);
script->name="script";
script->value.push_back(levelEventTypeMap[it->first]);
script->attributes["script"].push_back(it->second);
}
//Loop through the scenery layers and save them.
for (auto it = sceneryLayers.begin(); it != sceneryLayers.end(); ++it) {
TreeStorageNode* layer = new TreeStorageNode;
node.subNodes.push_back(layer);
layer->name = "scenerylayer";
layer->value.push_back(it->first);
it->second->saveToNode(layer);
}
//Create a POASerializer and write away the level node.
POASerializer objSerializer;
objSerializer.writeNode(&node,save,true,true);
}
void LevelEditor::deselectAll() {
selection.clear();
dragCenter = NULL;
selectionDrag = -1;
selectionDirty();
}
///////////////EVENT///////////////////
void LevelEditor::handleEvents(ImageManager& imageManager, SDL_Renderer& renderer){
//Check if we need to quit, if so we enter the exit state.
if(event.type==SDL_QUIT){
setNextState(STATE_EXIT);
}
//If playing/testing we should the game handle the events.
if(playMode){
Game::handleEvents(imageManager,renderer);
//Also check if we should exit the playMode.
if(inputMgr.isKeyDownEvent(INPUTMGR_ESCAPE)){
//Reset the game and disable playMode, disable script.
Game::reset(true, true);
playMode=false;
GUIObjectRoot->visible=true;
camera.x=cameraSave.x;
camera.y=cameraSave.y;
//NOTE: To prevent the mouse to still "be pressed" we set it to false.
pressedLeftMouse=false;
}
}else{
//Also check if we should exit the editor.
if(inputMgr.isKeyDownEvent(INPUTMGR_ESCAPE)){
std::string s;
//Check if the file is changed
if (commandManager->isChanged()) {
s = _("The level has unsaved changes.");
s.push_back('\n');
}
//Before we quit ask a make sure question.
if(msgBox(imageManager,renderer,s+_("Are you sure you want to quit?"),MsgBoxYesNo,_("Quit prompt"))==MsgBoxYes){
//We exit the level editor.
setNextState(STATE_LEVEL_EDIT_SELECT);
//Play the menu music again.
getMusicManager()->playMusic("menu");
//No need for handling other events, so return.
return;
}
}
//Check if we should redirect the event to the actions popup
if(actionsPopup!=NULL){
actionsPopup->handleEvents(renderer);
return;
}
//Check if we should redirect the event to selection popup
if(selectionPopup!=NULL){
if(event.type==SDL_MOUSEBUTTONDOWN
|| event.type==SDL_MOUSEBUTTONUP
|| event.type==SDL_MOUSEMOTION)
{
selectionPopup->handleEvents(imageManager,renderer);
return;
}
}
//TODO: Don't handle any Events when GUIWindows process them.
{
//Get the current mouse location.
int x,y;
SDL_GetMouseState(&x,&y);
//Create the rectangle.
SDL_Rect mouse={x,y,0,0};
for(unsigned int i=0;i<GUIObjectRoot->childControls.size();i++){
SDL_Rect box={0,0,0,0};
box.x=GUIObjectRoot->childControls[i]->left;
box.y=GUIObjectRoot->childControls[i]->top;
box.w=GUIObjectRoot->childControls[i]->width;
box.h=GUIObjectRoot->childControls[i]->height;
if(pointOnRect(mouse,box))
return;
}
}
//Check if toolbar is clicked.
if(event.type==SDL_MOUSEBUTTONDOWN && event.button.button==SDL_BUTTON_LEFT && tooltip>=0){
int t=tooltip;
if(t<NUMBER_TOOLS){
//Show/hide toolbox if the current mode is ADD and the user clicked ADD again.
if (tool == ADD && t == ADD) {
toolboxVisible = !toolboxVisible;
}
tool=(Tools)t;
//Stop linking or moving if the mode is not SELECT.
if (tool != SELECT) {
if (linking) {
linking = false;
linkingTrigger = NULL;
}
if (moving) {
moving = false;
movingBlock = NULL;
}
}
}else{
//The selected button isn't a tool.
//Now check which button it is.
if (t == (int)ToolTips::Play){
enterPlayMode();
}
if (t == (int)ToolTips::LevelSettings){
//Open up level settings dialog
levelSettings(imageManager,renderer);
}
if (t == (int)ToolTips::BackToMenu){
//If the file is changed we show a confirmation dialog
if (commandManager->isChanged()) {
std::string s = _("The level has unsaved changes.");
s.push_back('\n');
if (msgBox(imageManager, renderer, s + _("Are you sure you want to quit?"), MsgBoxYesNo, _("Quit prompt")) != MsgBoxYes) return;
}
//Go back to the level selection screen of Level Editor
setNextState(STATE_LEVEL_EDIT_SELECT);
//Change the music back to menu music.
getMusicManager()->playMusic("menu");
}
if (t == (int)ToolTips::SaveLevel){
//Save current level
saveCurrentLevel(imageManager, renderer);
}
if (t == (int)ToolTips::UndoNoTooltip) {
commandManager->undo();
}
if (t == (int)ToolTips::RedoNoTooltip) {
commandManager->redo();
}
}
return;
}
//Check if tool box is clicked.
if(event.type==SDL_MOUSEBUTTONDOWN && event.button.button==SDL_BUTTON_LEFT && toolboxRect.w>0){
if(toolboxVisible){
if(event.button.y<64){
//Check if we need to hide it
if(event.button.x>=SCREEN_WIDTH-24 && event.button.x<SCREEN_WIDTH && event.button.y<20){
toolboxVisible=false;
return;
}
const int m = (SCREEN_WIDTH - 48) / 64;
//Check if a block is clicked.
if(event.button.x>=24 && event.button.x<SCREEN_WIDTH-24){
int i=(event.button.x-24)/64;
if(i<m && i+toolboxIndex<getEditorOrderMax()){
currentType=i+toolboxIndex;
}
}
//Check if move left button is clicked
if (event.button.x >= 0 && event.button.x < 24 && event.button.y >= 20 && event.button.y < 44) {
toolboxIndex -= m;
if (toolboxIndex < 0) toolboxIndex = 0;
}
//Check if move right button is clicked
if (event.button.x >= SCREEN_WIDTH - 24 && event.button.x < SCREEN_WIDTH && event.button.y >= 20 && event.button.y < 44) {
toolboxIndex += m;
if (toolboxIndex > getEditorOrderMax() - m) toolboxIndex = getEditorOrderMax() - m;
if (toolboxIndex < 0) toolboxIndex = 0;
}
return;
}
}else if(event.button.x>=toolboxRect.x && event.button.x<toolboxRect.x+toolboxRect.w
&& event.button.y>=toolboxRect.y && event.button.y<toolboxRect.y+toolboxRect.h)
{
toolboxVisible=true;
return;
}
}
//Check if shift is pressed.
pressedShift=inputMgr.isKeyDown(INPUTMGR_SHIFT);
//Check if delete is pressed.
if (event.type == SDL_KEYDOWN && event.key.keysym.sym == SDLK_DELETE){
if (!selection.empty()){
AddRemoveGameObjectCommand *command = new AddRemoveGameObjectCommand(this, selection, false);
//clear the selection vector first.
deselectAll();
//perform the actual deletion.
commandManager->doCommand(command);
}
}
//Check for copy (Ctrl+c) or cut (Ctrl+x).
if(event.type==SDL_KEYDOWN && (event.key.keysym.sym==SDLK_c || event.key.keysym.sym==SDLK_x) && (event.key.keysym.mod & KMOD_CTRL)){
//Check if the selection isn't empty.
if(!selection.empty()){
//Clear the current clipboard.
clipboard.clear();
//Loop through the selection to find the left-top block.
int x=selection[0]->getBox().x;
int y=selection[0]->getBox().y;
for(unsigned int o=1; o<selection.size(); o++){
if(selection[o]->getBox().x<x || selection[o]->getBox().y<y){
x=selection[o]->getBox().x;
y=selection[o]->getBox().y;
}
}
//Loop through the selection for the actual copying.
for(unsigned int o=0; o<selection.size(); o++){
//Get the editor data of the object.
vector<pair<string,string> > obj;
selection[o]->getEditorData(obj);
//Loop through the editor data and convert it.
map<string,string> objMap;
for(unsigned int i=0;i<obj.size();i++){
objMap[obj[i].first]=obj[i].second;
}
//Add some entries to the map.
char s[64];
SDL_Rect r = selection[o]->getBox();
sprintf(s, "%d", r.x - x);
objMap["x"]=s;
sprintf(s, "%d", r.y - y);
objMap["y"]=s;
sprintf(s, "%d", selection[o]->getBox().w);
objMap["w"] = s;
sprintf(s, "%d", selection[o]->getBox().h);
objMap["h"] = s;
if (Scenery *scenery = dynamic_cast<Scenery*>(selection[o])) {
objMap["sceneryName"] = scenery->sceneryName_;
objMap["customScenery"] = scenery->customScenery_;
} else {
sprintf(s, "%d", selection[o]->type);
objMap["type"] = s;
//Save scripts for block.
if (Block *block = dynamic_cast<Block*>(selection[o])) {
for (auto it = block->scripts.begin(); it != block->scripts.end(); ++it) {
sprintf(s, "_script.%d", it->first);
objMap[s] = it->second;
}
}
}
//Overwrite the id to prevent triggers, portals, buttons, movingblocks, etc. from malfunctioning.
//We give an empty string as id, which is invalid and thus suitable.
objMap["id"]="";
//Do the same for destination if the type is portal.
if(selection[o]->type==TYPE_PORTAL){
objMap["destination"]="";
}
//And add the map to the clipboard vector.
clipboard.push_back(objMap);
}
//Cutting means deleting the game object.
if (event.key.keysym.sym == SDLK_x && !selection.empty()){
AddRemoveGameObjectCommand *command = new AddRemoveGameObjectCommand(this, selection, false);
//clear the selection vector first.
deselectAll();
//perform the actual deletion.
commandManager->doCommand(command);
}
}
}
//Check for paste (Ctrl+v).
if(event.type==SDL_KEYDOWN && event.key.keysym.sym==SDLK_v && (event.key.keysym.mod & KMOD_CTRL)){
//First make sure that the clipboard isn't empty.
if(!clipboard.empty()){
//Clear the current selection.
deselectAll();
//Get the current mouse location.
int x,y;
SDL_GetMouseState(&x,&y);
x+=camera.x;
y+=camera.y;
//Apply snap to grid.
if(!pressedShift){
snapToGrid(&x,&y);
}else{
x-=25;
y-=25;
}
std::vector<GameObject*> newObjects;
//Loop through the clipboard.
for(unsigned int o=0;o<clipboard.size();o++){
GameObject *obj = NULL;
if (clipboard[o].find("sceneryName") == clipboard[o].end()) {
// a normal block
if (!selectedLayer.empty()) continue;
Block *block = new Block(this, 0, 0, 50, 50, atoi(clipboard[o]["type"].c_str()));
obj = block;
// load the script for the block
for (auto it = clipboard[o].begin(); it != clipboard[o].end(); ++it) {
if (it->first.find("_script.") == 0) {
int eventType = atoi(it->first.c_str() + 8);
block->scripts[eventType] = it->second;
}
}
} else {
// a scenery block
if (selectedLayer.empty()) continue;
Scenery *scenery = new Scenery(this, 0, 0, 50, 50, clipboard[o]["sceneryName"]);
if (clipboard[o]["sceneryName"].empty()) {
scenery->customScenery_ = clipboard[o]["customScenery"];
scenery->updateCustomScenery(imageManager, renderer);
}
obj = scenery;
}
obj->setBaseLocation(atoi(clipboard[o]["x"].c_str())+x,atoi(clipboard[o]["y"].c_str())+y);
obj->setBaseSize(atoi(clipboard[o]["w"].c_str()), atoi(clipboard[o]["h"].c_str()));
obj->setEditorData(clipboard[o]);
//add the object.
newObjects.push_back(obj);
//Also add the block to the selection.
selection.push_back(obj);
}
// Do the actual object insertion
if (!newObjects.empty()) {
commandManager->doCommand(new AddRemoveGameObjectCommand(this, newObjects, true));
}
}
}
//Check for the arrow keys, used for moving the camera when playMode=false.
if (inputMgr.isKeyDown(INPUTMGR_RIGHT)) {
if (cameraXvel < 5) cameraXvel = 5;
} else if (inputMgr.isKeyDown(INPUTMGR_LEFT)) {
if (cameraXvel > -5) cameraXvel = -5;
} else {
cameraXvel = 0;
}
if(inputMgr.isKeyDown(INPUTMGR_DOWN)){
if (cameraYvel < 5) cameraYvel = 5;
} else if (inputMgr.isKeyDown(INPUTMGR_UP)){
if (cameraYvel > -5) cameraYvel = -5;
} else {
cameraYvel = 0;
}
//Check if the left mouse button is pressed/holded.
if(event.type==SDL_MOUSEBUTTONDOWN && event.button.button==SDL_BUTTON_LEFT){
pressedLeftMouse=true;
}
if(event.type==SDL_MOUSEBUTTONUP && event.button.button==SDL_BUTTON_LEFT){
pressedLeftMouse=false;
//We also need to check if dragging is true.
if(dragging){
//Set dragging false and call the onDrop event.
dragging=false;
int x,y;
SDL_GetMouseState(&x,&y);
//We call the drop event.
onDrop(x+camera.x,y+camera.y);
}
}
//Check if the mouse is dragging.
if(pressedLeftMouse && event.type==SDL_MOUSEMOTION) {
//Check if this is the start of the dragging.
if(!dragging){
//The mouse is moved enough so let's set dragging true.
dragging=true;
// NOTE: We start drag from previous mouse position to prevent resize area hit test bug
onDragStart(event.motion.x - event.motion.xrel + camera.x, event.motion.y - event.motion.yrel + camera.y);
onDrag(event.motion.xrel, event.motion.yrel);
} else {
//Dragging was already true meaning we call onDrag() instead of onDragStart().
onDrag(event.motion.xrel,event.motion.yrel);
}
}
//Update cursor.
if(dragging){
if (tool == REMOVE) {
currentCursor = CURSOR_REMOVE;
} else {
switch (selectionDrag) {
case 0: case 8:
currentCursor = CURSOR_SIZE_FDIAG;
break;
case 1: case 7:
currentCursor = CURSOR_SIZE_VER;
break;
case 2: case 6:
currentCursor = CURSOR_SIZE_BDIAG;
break;
case 3: case 5:
currentCursor = CURSOR_SIZE_HOR;
break;
case 4:
currentCursor = CURSOR_DRAG;
break;
default:
currentCursor = CURSOR_POINTER;
break;
}
}
}
//Get the current mouse location.
int x,y;
SDL_GetMouseState(&x,&y);
//Create the rectangle.
SDL_Rect mouse={x,y,0,0};
//Check if we scroll up, meaning the currentType++;
if((event.type==SDL_MOUSEWHEEL && event.wheel.y > 0) || inputMgr.isKeyDownEvent(INPUTMGR_NEXT)){
switch(tool){
case ADD:
//Check if mouse is in tool box.
if(toolboxVisible && toolboxRect.w>0){
int x,y;
SDL_GetMouseState(&x,&y);
if(y<64){
const int m = getEditorOrderMax() - (SCREEN_WIDTH - 48) / 64;
toolboxIndex -= 2;
if (toolboxIndex>m) toolboxIndex = m;
if (toolboxIndex<0) toolboxIndex = 0;
break;
}
}
//Only change the current type when using the add tool.
currentType++;
if (currentType >= getEditorOrderMax()){
currentType=0;
}
break;
case SELECT:
//When configuring moving blocks.
if(moving){
if (pauseMode) {
//Here we have to use Ctrl because Shift means snap
pauseTime += (SDL_GetModState() & KMOD_CTRL) ? 10 : 1;
} else {
//Here we have to use Ctrl because Shift means snap
movingSpeed += (SDL_GetModState() & KMOD_CTRL) ? 10 : 1;
//The movingspeed is capped at 125 (10 block/s).
if (movingSpeed > 125){
movingSpeed = 125;
}
}
break;
}
//Fall through.
default:
//When in other mode, just scrolling the map
if (pressedShift) camera.x = clamp(camera.x - 200, -1000 - SCREEN_WIDTH, LEVEL_WIDTH + 1000);
else camera.y = clamp(camera.y - 200, -1000 - SCREEN_HEIGHT, LEVEL_HEIGHT + 1000);
break;
}
}
//Check if we scroll down, meaning the currentType--;
if((event.type==SDL_MOUSEWHEEL && event.wheel.y < 0) || inputMgr.isKeyDownEvent(INPUTMGR_PREVIOUS)){
switch(tool){
case ADD:
//Check if mouse is in tool box.
if(toolboxVisible && toolboxRect.w>0){
int x,y;
SDL_GetMouseState(&x,&y);
if(y<64){
const int m = getEditorOrderMax() - (SCREEN_WIDTH - 48) / 64;
toolboxIndex+=2;
if(toolboxIndex>m) toolboxIndex=m;
if(toolboxIndex<0) toolboxIndex=0;
break;
}
}
//Only change the current type when using the add tool.
currentType--;
if(currentType<0){
currentType = getEditorOrderMax() - 1;
}
break;
case SELECT:
//When configuring moving blocks.
if(moving){
if (pauseMode) {
//Here we have to use Ctrl because Shift means snap
pauseTime -= (SDL_GetModState() & KMOD_CTRL) ? 10 : 1;
if (pauseTime < -1){
pauseTime = -1;
}
} else {
//Here we have to use Ctrl because Shift means snap
movingSpeed -= (SDL_GetModState() & KMOD_CTRL) ? 10 : 1;
if (movingSpeed <= 0){
movingSpeed = 1;
}
}
break;
}
//Fall through.
default:
//When in other mode, just scrolling the map
if (pressedShift) camera.x = clamp(camera.x + 200, -1000 - SCREEN_WIDTH, LEVEL_WIDTH + 1000);
else camera.y = clamp(camera.y + 200, -1000 - SCREEN_HEIGHT, LEVEL_HEIGHT + 1000);
break;
}
}
if (event.type == SDL_KEYDOWN) {
bool unlink = false;
//Check if we should enter playMode.
if (event.key.keysym.sym == SDLK_F5){
enterPlayMode();
}
//Check for tool shortcuts.
if (event.key.keysym.sym == SDLK_F2){
tool = SELECT;
}
if (event.key.keysym.sym == SDLK_F3){
//Show/hide toolbox if the current mode is ADD and the user clicked ADD again.
if (tool == ADD) {
toolboxVisible = !toolboxVisible;
}
tool = ADD;
unlink = true;
}
if (event.key.keysym.sym == SDLK_F4){
tool = REMOVE;
unlink = true;
}
//Stop linking or moving if the mode is not SELECT.
if (unlink) {
if (linking) {
linking = false;
linkingTrigger = NULL;
}
if (moving) {
moving = false;
movingBlock = NULL;
}
}
}
//Check for certain events.
//First make sure the mouse isn't above the toolbar.
if(!pointOnRect(mouse,toolbarRect) && !pointOnRect(mouse,toolboxRect)){
mouse.x+=camera.x;
mouse.y+=camera.y;
//Boolean if there's a click event fired.
bool clickEvent=false;
//Check if a mouse button is pressed.
if(event.type==SDL_MOUSEBUTTONDOWN){
//Right click in path or link mode means return to normal mode.
if (event.button.button == SDL_BUTTON_RIGHT && (linking || moving)) {
//Stop linking.
linking = false;
linkingTrigger = NULL;
//Stop moving.
moving = false;
movingBlock = NULL;
//Stop processing further.
return;
}
std::vector<GameObject*> clickObjects;
//Loop through the objects to check collision.
if (selectedLayer.empty()) {
if (layerVisibility[selectedLayer]) {
for (unsigned int o = 0; o<levelObjects.size(); o++){
if (pointOnRect(mouse, levelObjects[o]->getBox()) == true){
clickObjects.push_back(levelObjects[o]);
}
}
}
} else {
auto it = sceneryLayers.find(selectedLayer);
if (it != sceneryLayers.end() && layerVisibility[selectedLayer]) {
for (auto o : it->second->objects){
if (pointOnRect(mouse, o->getBox()) == true){
clickObjects.push_back(o);
}
}
}
}
//Check if there are multiple objects above eachother or just one.
if(clickObjects.size()==1){
//We have collision meaning that the mouse is above an object.
std::vector<GameObject*>::iterator it;
it=find(selection.begin(),selection.end(),clickObjects[0]);
//Set event true since there's a click event.
clickEvent=true;
//Check if the clicked object is in the selection or not.
bool isSelected=(it!=selection.end());
if(event.button.button==SDL_BUTTON_LEFT){
onClickObject(clickObjects[0],isSelected);
}else if(event.button.button==SDL_BUTTON_RIGHT){
onRightClickObject(imageManager,renderer,clickObjects[0],isSelected);
}
}else if(clickObjects.size()>=1){
//There are more than one object under the mouse
//SDL2 port (never managed to trigger this without changing the parameters.
std::vector<GameObject*>::iterator it;
it=find(selection.begin(),selection.end(),clickObjects[0]);
//Set event true since there's a click event.
clickEvent=true;
//Check if the clicked object is in the selection or not.
bool isSelected=(it!=selection.end());
//Only show the selection popup when right clicking.
if(event.button.button==SDL_BUTTON_LEFT){
onClickObject(clickObjects[0],isSelected);
}else if(event.button.button==SDL_BUTTON_RIGHT){
//Remove the selection popup if there's one.
if(selectionPopup!=NULL)
delete selectionPopup;
//Get the mouse location.
int x,y;
SDL_GetMouseState(&x,&y);
selectionPopup=new LevelEditorSelectionPopup(this,imageManager,renderer,clickObjects,x,y);
}
}
}
//If event is false then we clicked on void.
if(!clickEvent){
if(event.type==SDL_MOUSEBUTTONDOWN){
if(event.button.button==SDL_BUTTON_LEFT){
//Left mouse button on void.
onClickVoid(mouse.x,mouse.y);
}else if(event.button.button==SDL_BUTTON_RIGHT){
onRightClickVoid(imageManager,renderer,mouse.x,mouse.y);
}
}
}
}
//Check for backspace when moving to remove a movingposition.
if(moving && event.type==SDL_KEYDOWN && event.key.keysym.sym==SDLK_BACKSPACE){
if(movingBlocks[movingBlock].size()>0){
commandManager->doCommand(new AddRemovePathCommand(this, movingBlock, MovingPosition(0, 0, 0), false));
}
}
//Check for the tab key, level settings.
if(inputMgr.isKeyDownEvent(INPUTMGR_TAB)){
//Show the levelSettings.
levelSettings(imageManager,renderer);
}
//Check if we should save the level (Ctrl+s).
if(event.type==SDL_KEYDOWN && event.key.keysym.sym==SDLK_s && (event.key.keysym.mod & KMOD_CTRL)){
saveCurrentLevel(imageManager, renderer);
}
//Undo ctrl+z
if (event.type == SDL_KEYDOWN && event.key.keysym.sym == SDLK_z && (event.key.keysym.mod & KMOD_CTRL)){
undo();
}
//Redo ctrl+y
if (event.type == SDL_KEYDOWN && event.key.keysym.sym == SDLK_y && (event.key.keysym.mod & KMOD_CTRL)){
redo();
}
}
}
void LevelEditor::saveCurrentLevel(ImageManager& imageManager, SDL_Renderer& renderer) {
saveLevel(levelFile);
//Clear the dirty flag
commandManager->resetChange();
//And give feedback to the user.
if (levelName.empty())
msgBox(imageManager, renderer, tfm::format(_("Level \"%s\" saved"), fileNameFromPath(levelFile)), MsgBoxOKOnly, _("Saved"));
else
msgBox(imageManager, renderer, tfm::format(_("Level \"%s\" saved"), levelName), MsgBoxOKOnly, _("Saved"));
}
void LevelEditor::enterPlayMode(){
//Check if we are already in play mode.
if(playMode) return;
//Stop linking or moving.
if(linking){
linking=false;
linkingTrigger=NULL;
}
if(moving){
moving=false;
movingBlock=NULL;
}
//We need to reposition player and shadow here, since the related code is removed from object placement.
for (auto obj : levelObjects) {
//If the object is a player or shadow start then change the start position of the player or shadow.
if (obj->type == TYPE_START_PLAYER){
//Center the player horizontally.
player.fx = obj->getBox().x + (obj->getBox().w - player.getBox().w) / 2;
player.fy = obj->getBox().y;
}
if (obj->type == TYPE_START_SHADOW){
//Center the shadow horizontally.
shadow.fx = obj->getBox().x + (obj->getBox().w - shadow.getBox().w) / 2;
shadow.fy = obj->getBox().y;
}
}
//Change mode.
playMode=true;
GUIObjectRoot->visible=false;
cameraSave.x=camera.x;
cameraSave.y=camera.y;
//Restart the game with scripting enabled.
Game::reset(true, false);
}
void LevelEditor::undo(){
commandManager->undo();
}
void LevelEditor::redo(){
commandManager->redo();
}
void LevelEditor::levelSettings(ImageManager& imageManager,SDL_Renderer& renderer){
//It isn't so open a popup asking for a name.
GUIWindow* root=new GUIWindow(imageManager,renderer,(SCREEN_WIDTH-600)/2,(SCREEN_HEIGHT-450)/2,600,450,true,true,_("Level settings"));
root->minWidth = root->width; root->minHeight = root->height;
root->name="lvlSettingsWindow";
root->eventCallback=this;
GUIObject* obj;
//Create the two textboxes with a label.
obj=new GUILabel(imageManager,renderer,40,60,240,36,_("Name:"));
root->addChild(obj);
obj=new GUITextBox(imageManager,renderer,140,60,410,36,levelName.c_str());
obj->gravityRight = GUIGravityRight;
obj->name="name";
root->addChild(obj);
obj = new GUILabel(imageManager, renderer, 40, 110, 240, 36, (std::string("* ") + _("Theme:")).c_str());
root->addChild(obj);
obj=new GUITextBox(imageManager,renderer,140,110,410,36,levelTheme.c_str());
obj->gravityRight = GUIGravityRight;
obj->name="theme";
root->addChild(obj);
obj = new GUILabel(imageManager, renderer, 40, 150, 510, 36, _("Examples: %DATA%/themes/classic"));
root->addChild(obj);
obj = new GUILabel(imageManager, renderer, 40, 174, 510, 36, _("or %USER%/themes/Orange"));
root->addChild(obj);
obj = new GUILabel(imageManager, renderer, 40, 210, 240, 36, _("Music:"));
root->addChild(obj);
obj = new GUITextBox(imageManager, renderer, 140, 210, 410, 36, levelMusic.c_str());
obj->gravityRight = GUIGravityRight;
obj->name = "music";
root->addChild(obj);
//target time and recordings.
{
obj=new GUICheckBox(imageManager,renderer,40,260,240,36,_("Target time (s):"));
obj->name = "chkTime";
obj->value = levelTime >= 0 ? 1 : 0;
obj->eventCallback = root;
root->addChild(obj);
GUISpinBox* obj2=new GUISpinBox(imageManager,renderer,290,260,260,36);
obj2->gravityRight = GUIGravityRight;
obj2->name="time";
char ss[128];
sprintf(ss, "%0.2f", double(levelTime >= 0 ? levelTime : ~levelTime) / 40.0);
obj2->caption=ss;
obj2->visible = levelTime >= 0;
obj2->limitMin=0.0f;
obj2->limitMax = 1E+6f;
obj2->format = "%0.2f";
obj2->change=0.1f;
obj2->update();
root->addChild(obj2);
obj=new GUICheckBox(imageManager,renderer,40,310,240,36,_("Target recordings:"));
obj->name = "chkRecordings";
obj->value = levelRecordings >= 0 ? 1 : 0;
obj->eventCallback = root;
root->addChild(obj);
obj2=new GUISpinBox(imageManager,renderer,290,310,260,36);
obj2->gravityRight = GUIGravityRight;
sprintf(ss, "%d", levelRecordings >= 0 ? levelRecordings : ~levelRecordings);
obj2->caption=ss;
obj2->visible = levelRecordings >= 0;
obj2->limitMin=0.0f;
obj2->limitMax = 1E+6f;
obj2->format="%1.0f";
obj2->name="recordings";
obj2->update();
root->addChild(obj2);
}
obj = new GUILabel(imageManager, renderer, 40, 350, 510, 36, (std::string("* ") + _("Restart level editor is required")).c_str());
root->addChild(obj);
//Ok and cancel buttons.
obj=new GUIButton(imageManager,renderer,root->width*0.3,450-44,-1,36,_("OK"),0,true,true,GUIGravityCenter);
obj->gravityLeft = obj->gravityRight = GUIGravityCenter;
obj->gravityTop = obj->gravityBottom = GUIGravityRight;
obj->name="lvlSettingsOK";
obj->eventCallback=root;
root->addChild(obj);
obj=new GUIButton(imageManager,renderer,root->width*0.7,450-44,-1,36,_("Cancel"),0,true,true,GUIGravityCenter);
obj->gravityLeft = obj->gravityRight = GUIGravityCenter;
obj->gravityTop = obj->gravityBottom = GUIGravityRight;
obj->name="lvlSettingsCancel";
obj->eventCallback=root;
root->addChild(obj);
GUIObjectRoot->addChild(root);
}
void LevelEditor::postLoad(){
//We need to find the triggers.
for(unsigned int o=0;o<levelObjects.size();o++){
//Check for the highest id.
unsigned int id=atoi(levelObjects[o]->getEditorProperty("id").c_str());
if(id>=currentId)
currentId=id+1;
switch(levelObjects[o]->type){
case TYPE_BUTTON:
case TYPE_SWITCH:
{
//Add the object to the triggers vector.
vector<GameObject*> linked;
triggers[levelObjects[o]]=linked;
//Now loop through the levelObjects in search for objects with the same id.
for(unsigned int oo=0;oo<levelObjects.size();oo++){
//Check if it isn't the same object but has the same id.
if(o!=oo && (dynamic_cast<Block*>(levelObjects[o]))->id==(dynamic_cast<Block*>(levelObjects[oo]))->id){
//Add the object to the link vector of the trigger.
triggers[levelObjects[o]].push_back(levelObjects[oo]);
}
}
break;
}
case TYPE_PORTAL:
{
//Add the object to the triggers vector.
vector<GameObject*> linked;
triggers[levelObjects[o]]=linked;
//If the destination is empty we return.
if((dynamic_cast<Block*>(levelObjects[o]))->destination.empty()){
break;
}
//Now loop through the levelObjects in search for objects with the same id as destination.
for(unsigned int oo=0;oo<levelObjects.size();oo++){
//Check if it isn't the same object but has the same id.
if(o!=oo && (dynamic_cast<Block*>(levelObjects[o]))->destination==(dynamic_cast<Block*>(levelObjects[oo]))->id){
//Add the object to the link vector of the trigger.
triggers[levelObjects[o]].push_back(levelObjects[oo]);
}
}
break;
}
case TYPE_MOVING_BLOCK:
case TYPE_MOVING_SHADOW_BLOCK:
case TYPE_MOVING_SPIKES:
{
//Get the moving position.
const vector<SDL_Rect> &movingPos = levelObjects[o]->movingPos;
//Add the object to the movingBlocks vector.
movingBlocks[levelObjects[o]].clear();
for (int i = 0, m = movingPos.size(); i < m; i++) {
MovingPosition position(movingPos[i].x, movingPos[i].y, movingPos[i].w);
movingBlocks[levelObjects[o]].push_back(position);
}
break;
}
default:
break;
}
}
// Set the visibility of all layers to true
layerVisibility.clear();
for (auto it = sceneryLayers.begin(); it != sceneryLayers.end(); ++it) {
layerVisibility[it->first] = true;
}
layerVisibility[std::string()] = true;
// Also set the current layer to the Block layer
selectedLayer.clear();
// Get all available scenery blocks
std::set<std::string> tmpset;
objThemes.getSceneryBlockNames(tmpset);
sceneryBlockNames.clear();
sceneryBlockNames.insert(sceneryBlockNames.end(), tmpset.begin(), tmpset.end());
}
void LevelEditor::snapToGrid(int* x,int* y){
//Check if the x location is negative.
if(*x<0){
*x=-((abs(*x-50)/50)*50);
}else{
*x=(*x/50)*50;
}
//Now the y location.
if(*y<0){
*y=-((abs(*y-50)/50)*50);
}else{
*y=(*y/50)*50;
}
}
void LevelEditor::setCamera(const SDL_Rect* r,int count){
//SetCamera only works in the Level editor and when mouse is inside window.
if(stateID==STATE_LEVEL_EDITOR&&(SDL_GetMouseFocus() == sdlWindow)){
//Get the mouse coordinates.
int x,y;
SDL_GetMouseState(&x,&y);
SDL_Rect mouse={x,y,0,0};
//Don't continue here if mouse is inside one of the boxes given as parameter.
for(int i=0;i<count;i++){
if(pointOnRect(mouse,r[i]))
return;
}
//FIXME: two ad-hoc camera speed variables similar to cameraXvel and cameraYvel
static int cameraXvelB = 0, cameraYvelB = 0;
//Check if the mouse is near the left edge of the screen.
//Else check if the mouse is near the right edge.
if (x < 50) {
//We're near the left edge so move the camera.
if (cameraXvelB > -5) cameraXvelB = -5;
if (pressedShift) cameraXvelB--;
} else if (x > SCREEN_WIDTH - 50) {
//We're near the right edge so move the camera.
if (cameraXvelB < 5) cameraXvelB = 5;
if (pressedShift) cameraXvelB++;
} else {
cameraXvelB = 0;
}
//Check if the tool box is visible and we need to calc screen size correctly.
int y0=50;
if (toolboxVisible && toolboxRect.w > 0) y0 += toolbarRect.h;
//Check if the mouse is near the top edge of the screen.
//Else check if the mouse is near the bottom edge.
if (y < y0) {
//We're near the top edge so move the camera.
if (cameraYvelB > -5) cameraYvelB = -5;
if (pressedShift) cameraYvelB--;
} else if (y > SCREEN_HEIGHT - 50) {
//We're near the bottom edge so move the camera.
if (cameraYvelB < 5) cameraYvelB = 5;
if (pressedShift) cameraYvelB++;
} else {
cameraYvelB = 0;
}
camera.x = clamp(camera.x + cameraXvelB, -1000 - SCREEN_WIDTH, LEVEL_WIDTH + 1000);
camera.y = clamp(camera.y + cameraYvelB, -1000 - SCREEN_HEIGHT, LEVEL_HEIGHT + 1000);
}
}
void LevelEditor::onClickObject(GameObject* obj,bool selected){
switch(tool){
case SELECT:
{
//Check if we are linking.
if(linking){
//Check if the obj is valid to link to.
switch(obj->type){
case TYPE_CONVEYOR_BELT:
case TYPE_SHADOW_CONVEYOR_BELT:
case TYPE_MOVING_BLOCK:
case TYPE_MOVING_SHADOW_BLOCK:
case TYPE_MOVING_SPIKES:
{
//It's only valid when not linking a portal.
if(linkingTrigger->type==TYPE_PORTAL){
//You can't link a portal to moving blocks, etc.
//Stop linking and return.
linkingTrigger=NULL;
linking=false;
return;
}
break;
}
case TYPE_PORTAL:
{
//Make sure that the linkingTrigger is also a portal.
if(linkingTrigger->type!=TYPE_PORTAL){
//The linkingTrigger isn't a portal so stop linking and return.
linkingTrigger=NULL;
linking=false;
return;
}
break;
}
default:
//It isn't valid so stop linking and return.
linkingTrigger=NULL;
linking=false;
return;
break;
}
AddLinkCommand* pCommand = new AddLinkCommand(this, linkingTrigger, obj);
commandManager->doCommand(pCommand);
//We return to prevent configuring stuff like conveyor belts, etc...
linking=false;
linkingTrigger=NULL;
return;
}
//If we're moving add a movingposition.
if(moving){
//Get the current mouse location.
int x, y;
SDL_GetMouseState(&x, &y);
x += camera.x;
y += camera.y;
addMovingPosition(x, y);
return;
}
}
case ADD:
{
//Check if object is already selected.
if(!selected){
//First check if shift is pressed or not.
if(!pressedShift){
//Clear the selection.
deselectAll();
}
//Add the object to the selection.
selection.push_back(obj);
}
break;
}
case REMOVE:
{
//Remove the object.
commandManager->doCommand(new AddRemoveGameObjectCommand(this, obj, false));
break;
}
default:
break;
}
}
void LevelEditor::addMovingPosition(int x,int y) {
//Apply snap to grid.
if (!pressedShift){
snapToGrid(&x, &y);
} else{
x -= 25;
y -= 25;
}
x -= movingBlock->getBox().x;
y -= movingBlock->getBox().y;
//Calculate the length.
//First get the delta x and y.
int dx, dy;
if (movingBlocks[movingBlock].empty()){
dx = x;
dy = y;
} else{
dx = x - movingBlocks[movingBlock].back().x;
dy = y - movingBlocks[movingBlock].back().y;
}
AddRemovePathCommand* pCommand = NULL;
if (dx == 0 && dy == 0) {
// pause mode
if (pauseTime != 0) pCommand = new AddRemovePathCommand(this, movingBlock, MovingPosition(x, y, std::max(pauseTime, 0)), true);
pauseTime = 0;
} else {
// add new point mode
const double length = sqrt(double(dx*dx + dy*dy));
pCommand = new AddRemovePathCommand(this, movingBlock, MovingPosition(x, y, (int)(length*(10 / (double)movingSpeed))), true);
}
if (pCommand) commandManager->doCommand(pCommand);
}
void LevelEditor::onRightClickObject(ImageManager& imageManager,SDL_Renderer& renderer,GameObject* obj,bool){
//Create an actions popup for the game object.
if(actionsPopup==NULL){
//Get the mouse location.
int x,y;
SDL_GetMouseState(&x,&y);
actionsPopup=new LevelEditorActionsPopup(imageManager,renderer,this,obj,x,y);
return;
}
}
void LevelEditor::onClickVoid(int x,int y){
switch(tool){
case ADD:
{
//We need to clear the selection.
deselectAll();
//Now place an object.
//Apply snap to grid.
if(!pressedShift){
snapToGrid(&x,&y);
}else{
x-=25;
y-=25;
}
if (currentType >= 0 && currentType < getEditorOrderMax()) {
GameObject *obj;
if (selectedLayer.empty()) {
obj = new Block(this, x, y, 50, 50, editorTileOrder[currentType]);
} else {
obj = new Scenery(this, x, y, 50, 50,
currentType < (int)sceneryBlockNames.size() ? sceneryBlockNames[currentType] : std::string());
}
commandManager->doCommand(new AddRemoveGameObjectCommand(this, obj, true));
}
break;
}
case SELECT:
{
//We need to clear the selection.
deselectAll();
//If we're linking we should stop, user abort.
if(linking){
linking=false;
linkingTrigger=NULL;
//And return.
return;
}
//If we're moving we should add a point.
if(moving){
addMovingPosition(x, y);
//And return.
return;
}
break;
}
default:
break;
}
}
void LevelEditor::onRightClickVoid(ImageManager& imageManager,SDL_Renderer& renderer,int,int){
//Create an actions popup for the game object.
if(actionsPopup==NULL){
//Get the mouse location.
int x,y;
SDL_GetMouseState(&x,&y);
actionsPopup=new LevelEditorActionsPopup(imageManager,renderer,this,NULL,x,y);
return;
}
}
void LevelEditor::onDragStart(int x,int y){
switch(tool){
case SELECT:
case ADD:
{
//We can drag the selection so check if the selection isn't empty.
if(!selection.empty()){
//The selection isn't empty so search the dragCenter.
//Create a mouse rectangle.
SDL_Rect mouse={x,y,0,0};
// record the drag start position
dragSrartPosition.x = x;
dragSrartPosition.y = y;
//Loop through the objects to check collision.
for(unsigned int o=0; o<selection.size(); o++){
SDL_Rect r = selection[o]->getBox();
if(pointOnRect(mouse, r)){
//We have collision so set the dragCenter.
dragCenter=selection[o];
// determine which part is dragged
selectionDrag = 4;
int midx = r.x + r.w / 2 - 2;
int midy = r.y + r.h / 2 - 2;
if (mouse.x >= r.x && mouse.x < r.x + 5) {
if (mouse.y >= r.y && mouse.y < r.y + 5) {
selectionDrag = 0;
} else if (mouse.y >= midy && mouse.y < midy + 5) {
selectionDrag = 3;
} else if (mouse.y >= r.y + r.h - 5 && mouse.y < r.y + r.h) {
selectionDrag = 6;
}
} else if (mouse.x >= midx && mouse.x < midx + 5) {
if (mouse.y >= r.y && mouse.y < r.y + 5) {
selectionDrag = 1;
} else if (mouse.y >= r.y + r.h - 5 && mouse.y < r.y + r.h) {
selectionDrag = 7;
}
} else if (mouse.x >= r.x + r.w - 5 && mouse.x < r.x + r.w) {
if (mouse.y >= r.y && mouse.y < r.y + 5) {
selectionDrag = 2;
} else if (mouse.y >= midy && mouse.y < midy + 5) {
selectionDrag = 5;
} else if (mouse.y >= r.y + r.h - 5 && mouse.y < r.y + r.h) {
selectionDrag = 8;
}
}
break;
}
}
}
break;
}
default:
break;
}
}
void LevelEditor::onDrag(int dx,int dy){
switch(tool){
case REMOVE:
{
//No matter what we delete the item the mouse is above.
//Get the current mouse location.
int x,y;
SDL_GetMouseState(&x,&y);
//Create the rectangle.
SDL_Rect mouse={x+camera.x,y+camera.y,0,0};
currentCursor=CURSOR_REMOVE;
std::vector<GameObject*> objects;
//Loop through the objects to check collision.
if (selectedLayer.empty()) {
if (layerVisibility[selectedLayer]) {
for (unsigned int o = 0; o<levelObjects.size(); o++){
if (pointOnRect(mouse, levelObjects[o]->getBox()) == true){
objects.push_back(levelObjects[o]);
}
}
}
} else {
auto it = sceneryLayers.find(selectedLayer);
if (it != sceneryLayers.end() && layerVisibility[selectedLayer]) {
for (auto o : it->second->objects){
if (pointOnRect(mouse, o->getBox()) == true){
objects.push_back(o);
}
}
}
}
// Do the actual object deletion.
if (!objects.empty()) {
commandManager->doCommand(new AddRemoveGameObjectCommand(this, objects, false));
}
break;
}
default:
break;
}
}
void LevelEditor::onDrop(int x,int y){
switch(tool){
case SELECT:
case ADD:
{
//Check if the drag center isn't null.
if(dragCenter==NULL)
return;
//The location of the dragCenter.
SDL_Rect r=dragCenter->getBox();
if (selectionDrag == 4) { // dragging
//Apply snap to grid.
determineNewPosition(x, y);
commandManager->doCommand(new MoveGameObjectCommand(this, selection, x - r.x, y - r.y));
} else if (selectionDrag >= 0) { // resizing
determineNewSize(x, y, r);
commandManager->doCommand(new MoveGameObjectCommand(this, dragCenter, r.x, r.y, r.w, r.h));
}
//Make sure the dragCenter is null and set selectionDrag false.
dragCenter=NULL;
selectionDrag=-1;
break;
}
default:
break;
}
}
void LevelEditor::onCameraMove(int dx,int dy){
switch(tool){
case REMOVE:
{
//Only delete when the left mouse button is pressed.
if(pressedLeftMouse){
//Get the current mouse location.
int x,y;
SDL_GetMouseState(&x,&y);
//Create the rectangle.
SDL_Rect mouse={x+camera.x,y+camera.y,0,0};
std::vector<GameObject*> objects;
//Loop through the objects to check collision.
if (selectedLayer.empty()) {
if (layerVisibility[selectedLayer]) {
for (unsigned int o = 0; o<levelObjects.size(); o++){
if (pointOnRect(mouse, levelObjects[o]->getBox()) == true){
objects.push_back(levelObjects[o]);
}
}
}
} else {
auto it = sceneryLayers.find(selectedLayer);
if (it != sceneryLayers.end() && layerVisibility[selectedLayer]) {
for (auto o : it->second->objects){
if (pointOnRect(mouse, o->getBox()) == true){
objects.push_back(o);
}
}
}
}
// Do the actual object deletion.
if (!objects.empty()) {
commandManager->doCommand(new AddRemoveGameObjectCommand(this, objects, false));
}
}
break;
}
default:
break;
}
}
void LevelEditor::selectionDirty() {
if (selectionPopup != NULL) selectionPopup->dirty = true;
}
void LevelEditor::GUIEventCallback_OnEvent(ImageManager& imageManager, SDL_Renderer& renderer, std::string name,GUIObject* obj,int eventType){
//Check if one of the windows is closed.
if (eventType == GUIEventClick && name.size() >= 6 && name.substr(name.size() - 6) == "Window") {
destroyWindow(obj);
return;
}
//Resize code for each GUIWindow.
if (name.size() >= 6 && name.substr(name.size() - 6) == "Window") {
//Currently we don't need to process custom resize code since they are already processed in GUIWindow::resize().
return;
}
//Check for GUI events.
//Notification block configure events.
if(name=="cfgNotificationBlockOK"){
//Get the configuredObject.
GameObject* configuredObject=objectWindows[obj];
if(configuredObject){
//Get the message textbox from the GUIWindow.
GUITextArea* message=(GUITextArea*)obj->getChild("message");
if(message){
//Set the message of the notification block.
commandManager->doCommand(new SetEditorPropertyCommand(this, imageManager, renderer,
configuredObject, "message", message->getString(), _("Message")));
}
}
}
//Conveyor belt block configure events.
else if(name=="cfgConveyorBlockOK"){
//Get the configuredObject.
GameObject* configuredObject=objectWindows[obj];
if(configuredObject){
//Get the speed textbox from the GUIWindow.
GUISpinBox* speed=(GUISpinBox*)obj->getChild("speed");
if(speed){
//Set the speed of the conveyor belt.
commandManager->doCommand(new SetEditorPropertyCommand(this, imageManager, renderer,
configuredObject, "speed10", speed->caption, _("Speed")));
}
}
}
else if (name == "chkTime") {
obj->getChild("time")->visible = obj->getChild("chkTime")->value ? 1 : 0;
return;
}
else if (name == "chkRecordings") {
obj->getChild("recordings")->visible = obj->getChild("chkRecordings")->value ? 1 : 0;
return;
}
//LevelSetting events.
else if(name=="lvlSettingsOK"){
SetLevelPropertyCommand::LevelProperty prop;
prop.levelTime = -1;
prop.levelRecordings = -1;
GUIObject* object=obj->getChild("name");
if(object)
prop.levelName=object->caption;
object=obj->getChild("theme");
if(object)
prop.levelTheme=object->caption;
object = obj->getChild("music");
if (object)
prop.levelMusic = object->caption;
//target time and recordings.
object = obj->getChild("chkTime");
GUISpinBox* object2 = dynamic_cast<GUISpinBox*>(obj->getChild("time"));
assert(object && object2);
double number = std::max(atof(object2->caption.c_str()), 0.0);
prop.levelTime = int(floor(number*40.0 + 0.5));
if (object->value == 0) prop.levelTime = ~prop.levelTime;
object = obj->getChild("chkRecordings");
object2 = dynamic_cast<GUISpinBox*>(obj->getChild("recordings"));
assert(object && object2);
prop.levelRecordings = std::max(atoi(object2->caption.c_str()), 0);
if (object->value == 0) prop.levelRecordings = ~prop.levelRecordings;
// Perform the level setting modification
commandManager->doCommand(new SetLevelPropertyCommand(this, prop));
}
//Level scripting window events.
else if(name=="cfgLevelScriptingEventType"){
//Get the script textbox from the GUIWindow.
GUISingleLineListBox* list=(GUISingleLineListBox*)obj->getChild("cfgLevelScriptingEventType");
if(list){
//Loop through the scripts.
for(unsigned int i=0;i<list->item.size();i++){
GUIObject* script=obj->getChild(list->item[i].first);
if(script){
script->visible=(script->name==list->item[list->value].first);
script->enabled=(script->name==list->item[list->value].first);
}
}
}
return;
}
else if(name=="cfgLevelScriptingOK"){
//Get the script textbox from the GUIWindow.
GUISingleLineListBox* list=(GUISingleLineListBox*)obj->getChild("cfgLevelScriptingEventType");
if(list){
std::map<int, std::string> newScript;
//Loop through the scripts.
for(unsigned int i=0;i<list->item.size();i++){
//Get the GUITextArea.
GUITextArea* script=dynamic_cast<GUITextArea*>(obj->getChild(list->item[i].first));
if(script){
//Set the script for the target block.
string str=script->getString();
if(!str.empty())
newScript[levelEventNameMap[script->name]]=str;
}
}
// Check achievement
if (!newScript.empty()) {
statsMgr.newAchievement("helloworld");
}
// Do the actual changes
commandManager->doCommand(new SetScriptCommand(this, NULL, newScript));
}
}
//Scripting window events.
else if (name == "cfgScriptingEventType"){
//TODO: Save any unsaved scripts? (Or keep track of all scripts and save upon cfgScriptingOK?)
//Get the configuredObject.
Block* configuredObject=dynamic_cast<Block*>(objectWindows[obj]);
if(configuredObject){
//Get the script textbox from the GUIWindow.
GUISingleLineListBox* list=dynamic_cast<GUISingleLineListBox*>(obj->getChild("cfgScriptingEventType"));
if(list){
//Loop through the scripts.
for(unsigned int i=0;i<list->item.size();i++){
GUIObject* script=obj->getChild(list->item[i].first);
if(script){
script->visible=script->enabled=(script->name==list->item[list->value].first);
}
}
}
}
return;
}
else if(name=="cfgScriptingOK"){
//Get the configuredObject.
Block* block = dynamic_cast<Block*>(objectWindows[obj]);
if (block){
std::map<int, std::string> newScript;
std::string newId;
//Get the script textbox from the GUIWindow.
GUISingleLineListBox* list=(GUISingleLineListBox*)obj->getChild("cfgScriptingEventType");
GUIObject* id=obj->getChild("id");
if (list){
//Loop through the scripts.
for (unsigned int i = 0; i < list->item.size(); i++){
//Get the GUITextArea.
GUITextArea* script = dynamic_cast<GUITextArea*>(obj->getChild(list->item[i].first));
if (script){
//Set the script for the target block.
string str = script->getString();
if (!str.empty())
newScript[gameObjectEventNameMap[script->name]] = str;
}
}
}
newId = block->id;
if (id){
newId = id->caption;
}
// Check achievement
if (!newScript.empty()) {
statsMgr.newAchievement("helloworld");
}
// now do the actual changes
commandManager->doCommand(new SetScriptCommand(this, block, newScript, newId));
}
}
else if (name == "cfgAddLayerOK") {
GUIObject* object = obj->getChild("layerName");
if (!object) return;
if (object->caption.empty()) {
msgBox(imageManager, renderer, _("Please enter a layer name."), MsgBoxOKOnly, _("Error"));
return;
}
if (sceneryLayers.find(object->caption) != sceneryLayers.end()) {
msgBox(imageManager, renderer, tfm::format(_("The layer '%s' already exists."), object->caption), MsgBoxOKOnly, _("Error"));
return;
}
// do the actual operation
commandManager->doCommand(new AddRemoveLayerCommand(this, object->caption, true));
}
else if (name == "cfgLayerSettingsOK") {
SetLayerPropertyCommand::LayerProperty prop;
GUIObject* object = obj->getChild("layerName");
if (!object) return;
prop.name = object->caption;
object = obj->getChild("speedX");
if (!object) return;
prop.speedX = atof(object->caption.c_str());
object = obj->getChild("speedY");
if (!object) return;
prop.speedY = atof(object->caption.c_str());
object = obj->getChild("cameraX");
if (!object) return;
prop.cameraX = atof(object->caption.c_str());
object = obj->getChild("cameraY");
if (!object) return;
prop.cameraY = atof(object->caption.c_str());
object = obj->getChild("oldName");
if (!object) return;
const std::string& oldName = object->caption;
if (prop.name.empty()) {
msgBox(imageManager, renderer, _("Please enter a layer name."), MsgBoxOKOnly, _("Error"));
return;
}
if (prop.name != oldName && sceneryLayers.find(prop.name) != sceneryLayers.end()) {
msgBox(imageManager, renderer, tfm::format(_("The layer '%s' already exists."), prop.name), MsgBoxOKOnly, _("Error"));
return;
}
// do the actual operation
commandManager->doCommand(new SetLayerPropertyCommand(this, oldName, prop));
}
else if (name == "cfgMoveToLayerOK") {
GUIObject* object = obj->getChild("layerName");
if (!object) return;
const std::string& layerName = object->caption;
object = obj->getChild("oldName");
if (!object) return;
const std::string& oldName = object->caption;
if (layerName.empty()) {
msgBox(imageManager, renderer, _("Please enter a layer name."), MsgBoxOKOnly, _("Error"));
return;
}
if (oldName == layerName) {
msgBox(imageManager, renderer, _("Source and destination layers are the same."), MsgBoxOKOnly, _("Error"));
return;
}
// do the actual operation
commandManager->doCommand(new MoveToLayerCommand(this, selection, oldName, layerName));
}
else if (name == "cfgCustomSceneryOK") {
//Get the configuredObject.
Scenery* configuredObject = dynamic_cast<Scenery*>(objectWindows[obj]);
if (configuredObject){
//Get the custom scenery from the GUIWindow.
GUITextArea* txt = (GUITextArea*)obj->getChild("cfgCustomScenery");
if (txt){
//Set the custom scenery.
commandManager->doCommand(new SetEditorPropertyCommand(this, imageManager, renderer,
configuredObject, "customScenery", txt->getString(), _("Scenery")));
}
}
}
else if (name == "lstAppearance") {
//Get the configuredObject.
Block *block = dynamic_cast<Block*>(objectWindows[obj]);
GUIListBox *list = dynamic_cast<GUIListBox*>(obj->getChild("lstAppearance"));
GUIImage *image = dynamic_cast<GUIImage*>(obj->getChild("imgAppearance"));
if (block && list && image) {
//Reset the image first.
image->setImage(NULL);
image->setClipRect(SDL_Rect{ 0, 0, 0, 0 });
//Get the appearance name.
std::string appearanceName;
if (list->value <= 0) {
//Do nothing since the selected is the default appearance.
} else if (list->value <= (int)sceneryBlockNames.size()) {
//A custom appearance is selected.
appearanceName = sceneryBlockNames[list->value - 1];
} else {
//The configured object has an invalid custom appearance name.
appearanceName = block->customAppearanceName;
}
//Try to find the theme block.
ThemeBlock *themeBlock = NULL;
if (!appearanceName.empty()) {
themeBlock = objThemes.getScenery(appearanceName);
}
if (themeBlock == NULL) {
themeBlock = objThemes.getBlock(block->type);
}
if (themeBlock) {
image->setImage(themeBlock->editorPicture.texture);
const auto& offsetData = themeBlock->editorPicture.offset.offsetData;
if (offsetData.size() > 0) {
const auto& r = offsetData[0];
image->setClipRect(SDL_Rect{ r.x, r.y, r.w, r.h });
}
}
}
return;
}
else if (name == "cfgAppearanceOK") {
//Get the configuredObject.
Block *block = dynamic_cast<Block*>(objectWindows[obj]);
GUIListBox *list = dynamic_cast<GUIListBox*>(obj->getChild("lstAppearance"));
if (block && list) {
//Get the appearance name.
std::string appearanceName;
if (list->value <= 0) {
//Do nothing since the selected is the default appearance.
} else if (list->value <= (int)sceneryBlockNames.size()) {
//A custom appearance is selected.
appearanceName = sceneryBlockNames[list->value - 1];
} else {
//The configured object has an invalid custom appearance name.
appearanceName = block->customAppearanceName;
}
//Update the block property if it's changed.
if (appearanceName != block->customAppearanceName) {
commandManager->doCommand(new SetEditorPropertyCommand(this, imageManager, renderer,
block, "appearance", appearanceName, _("Appearance")));
}
}
}
//NOTE: We assume every event came from a window
//and the event is either window closed event or OK/Cancel button click event, so we remove it.
destroyWindow(obj);
}
void LevelEditor::destroyWindow(GUIObject* window){
//Make sure the given pointer isn't null.
if(!window)
return;
//Remove the window from the GUIObject root.
if(GUIObjectRoot){
vector<GUIObject*>::iterator it;
it=find(GUIObjectRoot->childControls.begin(),GUIObjectRoot->childControls.end(),window);
if(it!=GUIObjectRoot->childControls.end()){
GUIObjectRoot->childControls.erase(it);
}
}
//Also remove the window from the objectWindows map.
map<GUIObject*,GameObject*>::iterator it;
it=objectWindows.find(window);
if(it!=objectWindows.end()){
objectWindows.erase(it);
}
//And delete the GUIWindow.
delete window;
}
////////////////LOGIC////////////////////
void LevelEditor::logic(ImageManager& imageManager, SDL_Renderer& renderer){
if(playMode){
//PlayMode so let the game do it's logic.
Game::logic(imageManager,renderer);
}else{
//Update animation even under edit mode. (There are checks in Block::move() which don't do game logic in edit mode.)
for (unsigned int i = 0; i<levelObjects.size(); i++){
//Let the gameobject handle movement.
levelObjects[i]->move();
}
//Also update the scenery.
for (auto it = sceneryLayers.begin(); it != sceneryLayers.end(); ++it){
it->second->updateAnimation();
}
//In case of a selection or actions popup prevent the camera from moving.
if(selectionPopup || actionsPopup)
return;
//Move the camera.
if (cameraXvel != 0 || cameraYvel != 0) {
if (pressedShift) {
if (cameraXvel > 0) cameraXvel++;
else if (cameraXvel < 0) cameraXvel--;
if (cameraYvel > 0) cameraYvel++;
else if (cameraYvel < 0) cameraYvel--;
}
camera.x = clamp(camera.x + cameraXvel, -1000 - SCREEN_WIDTH, LEVEL_WIDTH + 1000);
camera.y = clamp(camera.y + cameraYvel, -1000 - SCREEN_HEIGHT, LEVEL_HEIGHT + 1000);
//Call the onCameraMove event.
onCameraMove(cameraXvel, cameraYvel);
}
//Move the camera with the mouse.
//Get the mouse location.
int x,y;
SDL_GetMouseState(&x,&y);
SDL_Rect mouse={x,y,0,0};
{
//Check if the mouse isn't above a GUIObject (window).
bool inside=false;
for(unsigned int i=0;i<GUIObjectRoot->childControls.size();i++){
SDL_Rect box={0,0,0,0};
box.x=GUIObjectRoot->childControls[i]->left;
box.y=GUIObjectRoot->childControls[i]->top;
box.w=GUIObjectRoot->childControls[i]->width;
box.h=GUIObjectRoot->childControls[i]->height;
if(pointOnRect(mouse,box))
inside=true;
}
if(!inside){
SDL_Rect r[3]={toolbarRect,toolboxRect};
int m=2;
//TODO: Also call onCameraMove when moving using the mouse.
setCamera(r,m);
}
}
//It isn't playMode so the mouse should be checked.
tooltip=-1;
//We loop through the number of tools + the number of buttons.
for (int t = 0; t <= (int)ToolTips::BackToMenu; t++){
SDL_Rect toolRect={(SCREEN_WIDTH-460)/2+(t*40)+((t+1)*10),SCREEN_HEIGHT-45,40,40};
//Check for collision.
if(pointOnRect(mouse,toolRect)==true){
//Set the tooltip tool.
tooltip=t;
}
}
}
}
/////////////////RENDER//////////////////////
void LevelEditor::render(ImageManager& imageManager,SDL_Renderer& renderer){
//Let the game render the game when it is the play mode.
if (playMode) {
Game::render(imageManager, renderer);
} else {
// The following code are partially copied from Game::render()
//First of all render the background.
{
//Get a pointer to the background.
ThemeBackground* bg = background;
//Check if the background is null, but there are themes.
if (bg == NULL && objThemes.themeCount()>0){
//Get the background from the first theme in the stack.
bg = objThemes[0]->getBackground(false);
}
//Check if the background isn't null.
if (bg){
//It isn't so draw it.
bg->draw(renderer);
//And if it's the loaded background then also update the animation.
//FIXME: Updating the animation in the render method?
if (bg == background)
bg->updateAnimation();
} else{
//There's no background so fill the screen with white.
SDL_SetRenderDrawColor(&renderer, 255, 255, 255, 255);
SDL_RenderClear(&renderer);
}
}
//Now draw the background layers.
auto it = sceneryLayers.begin();
for (; it != sceneryLayers.end(); ++it){
if (it->first >= "f") break; // now we meet a foreground layer
if (layerVisibility[it->first]) {
it->second->show(renderer);
}
}
//Now we draw the levelObjects.
if (layerVisibility[std::string()]) {
//NEW: always render the pushable blocks in front of other blocks
std::vector<Block*> pushableBlocks;
for (auto o : levelObjects) {
if (o->type == TYPE_PUSHABLE) {
pushableBlocks.push_back(o);
} else {
o->show(renderer);
}
}
for (auto o : pushableBlocks) {
o->show(renderer);
}
}
//We don't draw the player and the shadow at all.
//Now draw the foreground layers.
for (; it != sceneryLayers.end(); ++it){
if (layerVisibility[it->first]) {
it->second->show(renderer);
}
}
}
//Only render extra stuff like the toolbar, selection, etc.. when not in playMode.
if(!playMode){
//Get the current mouse location.
int x, y;
SDL_GetMouseState(&x, &y);
//Create the rectangle.
SDL_Rect mouse = { x + camera.x, y + camera.y, 0, 0 };
//Render the selectionmarks.
//TODO: Check if block is in sight.
for(unsigned int o=0; o<selection.size(); o++){
//Get the location to draw.
SDL_Rect r=selection[o]->getBox();
// Change the mouse cursor if necessary
if (selectionDrag < 0 && tool != REMOVE) {
int midx = r.x + r.w / 2 - 2;
int midy = r.y + r.h / 2 - 2;
if (mouse.x >= r.x && mouse.x < r.x + 5) {
if (mouse.y >= r.y && mouse.y < r.y + 5) {
currentCursor = CURSOR_SIZE_FDIAG;
} else if (mouse.y >= midy && mouse.y < midy + 5) {
currentCursor = CURSOR_SIZE_HOR;
} else if (mouse.y >= r.y + r.h - 5 && mouse.y < r.y + r.h) {
currentCursor = CURSOR_SIZE_BDIAG;
}
} else if (mouse.x >= midx && mouse.x < midx + 5) {
if (mouse.y >= r.y && mouse.y < r.y + 5) {
currentCursor = CURSOR_SIZE_VER;
} else if (mouse.y >= r.y + r.h - 5 && mouse.y < r.y + r.h) {
currentCursor = CURSOR_SIZE_VER;
}
} else if (mouse.x >= r.x + r.w - 5 && mouse.x < r.x + r.w) {
if (mouse.y >= r.y && mouse.y < r.y + 5) {
currentCursor = CURSOR_SIZE_BDIAG;
} else if (mouse.y >= midy && mouse.y < midy + 5) {
currentCursor = CURSOR_SIZE_HOR;
} else if (mouse.y >= r.y + r.h - 5 && mouse.y < r.y + r.h) {
currentCursor = CURSOR_SIZE_FDIAG;
}
}
}
bool mouseIn = pointOnRect(mouse, r);
r.x-=camera.x;
r.y-=camera.y;
drawGUIBox(r.x,r.y,r.w,r.h,renderer,0xFFFFFF33);
//Draw the selectionMarks.
applyTexture(r.x,r.y,selectionMark,renderer);
applyTexture(r.x+r.w-5,r.y,selectionMark,renderer);
applyTexture(r.x,r.y+r.h-5,selectionMark,renderer);
applyTexture(r.x+r.w-5,r.y+r.h-5,selectionMark,renderer);
// draw additional selection marks
if (mouseIn && selectionDrag < 0 && tool != REMOVE) {
applyTexture(r.x + r.w / 2 - 2, r.y, selectionMark, renderer);
applyTexture(r.x + r.w / 2 - 2, r.y + r.h - 5, selectionMark, renderer);
applyTexture(r.x, r.y + r.h / 2 - 2, selectionMark, renderer);
applyTexture(r.x + r.w - 5, r.y + r.h / 2 - 2, selectionMark, renderer);
}
}
//Set the color for the borders.
{
SDL_Color c = objThemes.getTextColor(false);
SDL_SetRenderDrawColor(&renderer, c.r, c.g, c.b, 115);
}
int leftWidth=0;
int rightWidth=0;
//Draw the dark areas marking the outside of the level.
SDL_Rect r{0,0,0,0};
if(camera.x<0){
//Draw left side.
r.x=0;
r.y=0;
r.w=0-camera.x;
leftWidth=r.w;
r.h=SCREEN_HEIGHT;
SDL_RenderFillRect(&renderer, &r);
}
if(camera.y<0){
//Draw the top.
r.x=leftWidth;
r.y=0;
r.w=SCREEN_WIDTH;
r.h=0-camera.y;
SDL_RenderFillRect(&renderer, &r);
} else {
r.h=0;
}
if(camera.x>LEVEL_WIDTH-SCREEN_WIDTH){
//Draw right side.
r.x=LEVEL_WIDTH-camera.x;
r.y=std::max(r.y+r.h,0);
r.w=SCREEN_WIDTH-(LEVEL_WIDTH-camera.x);
rightWidth=r.w;
r.h=SCREEN_HEIGHT;
SDL_RenderFillRect(&renderer, &r);
}
if(camera.y>LEVEL_HEIGHT-SCREEN_HEIGHT){
//Draw the bottom.
r.x=leftWidth;
r.y=LEVEL_HEIGHT-camera.y;
r.w=SCREEN_WIDTH-rightWidth-leftWidth;
r.h=SCREEN_HEIGHT-(LEVEL_HEIGHT-camera.y);
SDL_RenderFillRect(&renderer, &r);
}
//Check if we should draw on stuff.
showConfigure(renderer);
if (selectionDrag >= 0 && tool != REMOVE) {
showSelectionDrag(renderer);
}
//Find a block where the mouse is hovering on.
bool isMouseOnSomething = false;
if (selectedLayer.empty()){
if (layerVisibility[selectedLayer]) {
// Current layer is Blocks layer
for (unsigned int o = 0; o<levelObjects.size(); o++){
SDL_Rect rect = levelObjects[o]->getBox();
if (pointOnRect(mouse, rect) == true){
isMouseOnSomething = true;
if (tool == REMOVE){
drawGUIBox(rect.x - camera.x, rect.y - camera.y, rect.w, rect.h, renderer, 0xFF000055);
currentCursor = CURSOR_REMOVE;
} else{
drawGUIBox(rect.x - camera.x, rect.y - camera.y, rect.w, rect.h, renderer, 0xFFFFFF33);
}
}
}
}
} else {
auto it = sceneryLayers.find(selectedLayer);
if (it != sceneryLayers.end() && layerVisibility[selectedLayer]) {
// Current layer is scenery layer
for (auto o : it->second->objects){
SDL_Rect rect = o->getBox();
if (pointOnRect(mouse, rect) == true){
isMouseOnSomething = true;
if (tool == REMOVE){
drawGUIBox(rect.x - camera.x, rect.y - camera.y, rect.w, rect.h, renderer, 0xFF000055);
currentCursor = CURSOR_REMOVE;
} else{
drawGUIBox(rect.x - camera.x, rect.y - camera.y, rect.w, rect.h, renderer, 0xFFFFFF33);
}
}
}
}
}
// show current object only when mouse is not hover on any blocks
if (!isMouseOnSomething && tool == ADD && selectionDrag < 0) {
showCurrentObject(renderer);
}
//Draw the level borders.
drawRect(-camera.x,-camera.y,LEVEL_WIDTH,LEVEL_HEIGHT,renderer);
//Render the hud layer.
renderHUD(renderer);
//Render selection popup (if any).
if(selectionPopup!=NULL){
if(linking || moving){
//If we switch to linking mode then delete it
//FIXME: Logic in the render method.
delete selectionPopup;
selectionPopup=NULL;
}else{
selectionPopup->render(imageManager,renderer);
}
}
//Render actions popup (if any).
if(actionsPopup!=NULL){
actionsPopup->render(renderer);
}
}
}
void LevelEditor::renderHUD(SDL_Renderer& renderer){
//If moving show the moving speed in the top right corner.
if(moving){
//Calculate width of text "Movespeed: 125" to keep the same position with every value
if (movingSpeedWidth == -1){
int w;
TTF_SizeUTF8(fontText, tfm::format(_("Speed: %d = %0.2f block/s"), 125, 10.0f).c_str(), &w, NULL);
movingSpeedWidth = w + 4;
}
SDL_Texture *tex = NULL;
//Check which text should we use.
if (pauseMode) {
//Update the text if necessary.
if (pauseTimeTexture.needsUpdate(pauseTime)) {
if (pauseTime < 0) {
pauseTimeTexture.update(pauseTime,
textureFromText(renderer, *fontText,
_("Stop at this point"),
objThemes.getTextColor(true)));
} else {
pauseTimeTexture.update(pauseTime,
textureFromText(renderer, *fontText,
tfm::format(_("Pause: %d = %0.3fs"), pauseTime, float(pauseTime)*0.025f).c_str(),
objThemes.getTextColor(true)));
}
}
tex = pauseTimeTexture.get();
} else {
//Update the text if necessary.
if (movementSpeedTexture.needsUpdate(movingSpeed)) {
movementSpeedTexture.update(movingSpeed,
textureFromText(renderer, *fontText,
tfm::format(_("Speed: %d = %0.2f block/s"), movingSpeed, float(movingSpeed)*0.08f).c_str(),
objThemes.getTextColor(true)));
}
tex = movementSpeedTexture.get();
}
//Draw the text in the box.
drawGUIBox(SCREEN_WIDTH-movingSpeedWidth-2,-2,movingSpeedWidth+8,
textureHeight(*tex)+6,renderer,0xFFFFFFFF);
applyTexture(SCREEN_WIDTH-movingSpeedWidth,2,*tex,renderer,NULL);
}
//On top of all render the toolbar.
drawGUIBox(toolbarRect.x,toolbarRect.y,9*50+10,52,renderer,0xEDEDEDFF);
//Draw the first four options.
SDL_Rect srcRect={0,0,200,50};
SDL_Rect dstRect={toolbarRect.x+5, toolbarRect.y, srcRect.w, srcRect.h};
SDL_RenderCopy(&renderer, toolbar.get(), &srcRect, &dstRect);
//Draw the undo/redo button.
SDL_SetTextureAlphaMod(toolbar.get(), commandManager->canUndo() ? 255 : 128);
srcRect.x = 200;
srcRect.w = 50;
dstRect.x = toolbarRect.x + 205;
dstRect.w = srcRect.w;
SDL_RenderCopy(&renderer, toolbar.get(), &srcRect, &dstRect);
SDL_SetTextureAlphaMod(toolbar.get(), commandManager->canRedo() ? 255 : 128);
srcRect.x = 250;
srcRect.w = 50;
dstRect.x = toolbarRect.x + 255;
dstRect.w = srcRect.w;
SDL_RenderCopy(&renderer, toolbar.get(), &srcRect, &dstRect);
SDL_SetTextureAlphaMod(toolbar.get(), 255);
//And the last three.
srcRect.x=300;
srcRect.w=150;
dstRect.x=toolbarRect.x+305;
dstRect.w=srcRect.w;
SDL_RenderCopy(&renderer, toolbar.get(), &srcRect, &dstRect);
//Now render a tooltip.
if(tooltip>=0 && static_cast<std::size_t>(tooltip)<tooltipTextures.size()) {
SDL_Texture *tex = tooltipTextures.at(tooltip).get();
if (tooltip == (int)ToolTips::UndoNoTooltip) {
std::string s = commandManager->describeUndo();
if (undoTooltipTexture.needsUpdate(s)) {
undoTooltipTexture.update(s, textureFromText(renderer, *fontText, s.c_str(), objThemes.getTextColor(true)));
}
tex = undoTooltipTexture.get();
} else if (tooltip == (int)ToolTips::RedoNoTooltip) {
std::string s = commandManager->describeRedo();
if (redoTooltipTexture.needsUpdate(s)) {
redoTooltipTexture.update(s, textureFromText(renderer, *fontText, s.c_str(), objThemes.getTextColor(true)));
}
tex = redoTooltipTexture.get();
}
if(tex) {
const SDL_Rect texSize = rectFromTexture(*tex);
SDL_Rect r={(SCREEN_WIDTH-440)/2+(tooltip*40)+(tooltip*10),SCREEN_HEIGHT-45,40,40};
r.y=SCREEN_HEIGHT-50-texSize.h;
if(r.x+texSize.w>SCREEN_WIDTH-50)
r.x=SCREEN_WIDTH-50-texSize.w;
//Draw borders around text
Uint32 color=0xFFFFFF00|230;
drawGUIBox(r.x-2,r.y-2,texSize.w+4,texSize.h+4,renderer,color);
applyTexture(r.x, r.y, *tex, renderer);
}
}
// for toolbox button animation (0-31)
static int tick = 8;
const int mmm = getEditorOrderMax();
if (currentType >= mmm)currentType = mmm - 1;
if (currentType < 0) currentType = 0;
//Render the tool box.
if(!playMode && !moving && tool==ADD && selectionPopup==NULL && actionsPopup==NULL && objectWindows.empty()){
// get mouse position
int x, y;
SDL_GetMouseState(&x, &y);
if (toolboxVisible){
toolboxRect.x=0;
toolboxRect.y=0;
toolboxRect.w=SCREEN_WIDTH;
toolboxRect.h=64;
drawGUIBox(-2,-2,SCREEN_WIDTH+4,66,renderer,0xFFFFFF00|230);
bool isMouseOnSomething = false;
//Draw the hide icon.
SDL_Rect r={SCREEN_WIDTH-20,2,16,16};
SDL_Rect r2={80,0,r.w,r.h};
if (x >= SCREEN_WIDTH - 24 && x < SCREEN_WIDTH && y < 20) {
isMouseOnSomething = true;
tick = (tick + 1) & 31;
r.y -= (tick < 16) ? (tick / 4 - 2) : (6 - tick / 4);
}
SDL_RenderCopy(&renderer, bmGUI.get(), &r2, &r);
//Calculate the maximal number of blocks can be displayed.
const int m=(SCREEN_WIDTH-48)/64;
if(toolboxIndex>=mmm-m){
toolboxIndex = mmm - m;
}else{
//Draw an icon.
r.x=SCREEN_WIDTH-20;
r.y=24;
r2.x=96;
r2.y=16;
if (x >= SCREEN_WIDTH - 24 && x < SCREEN_WIDTH && y >= 20 && y < 44) {
isMouseOnSomething = true;
tick = (tick + 1) & 31;
r.x += (tick < 16) ? (tick / 4 - 2) : (6 - tick / 4);
}
SDL_RenderCopy(&renderer, bmGUI.get(),&r2,&r);
}
if(toolboxIndex<=0){
toolboxIndex=0;
}else{
//Draw an icon.
r.x=4;
r.y=24;
r2.x=80;
r2.y=16;
if (x >= 0 && x < 24 && y >= 20 && y < 44) {
isMouseOnSomething = true;
tick = (tick + 1) & 31;
r.x -= (tick < 16) ? (tick / 4 - 2) : (6 - tick / 4);
}
SDL_RenderCopy(&renderer, bmGUI.get(), &r2, &r);
}
// reset animation timer if there is no animation
if (!isMouseOnSomething) {
tick = 8;
}
//Draw available blocks.
for(int i=0;i<m;i++){
if (i + toolboxIndex >= mmm) break;
//Draw a rectangle around the current tool.
if(i+toolboxIndex==currentType){
drawGUIBox(i*64+24,3,64,58,renderer,0xDDDDDDFF);
}
if (selectedLayer.empty()) {
// show normal blocks
ThemeBlock* obj = objThemes.getBlock(editorTileOrder[i + toolboxIndex]);
if (obj){
obj->editorPicture.draw(renderer, i * 64 + 24 + 7, 7);
}
} else {
// show scenery blocks
if (i + toolboxIndex < (int)sceneryBlockNames.size()) {
ThemeBlock* obj = objThemes.getScenery(sceneryBlockNames[i + toolboxIndex]);
if (obj){
obj->editorPicture.draw(renderer, i * 64 + 24 + 7, 7);
}
} else {
// it's custom scenery block
// just draw a stupid icon
const SDL_Rect r = { 48, 16, 16, 16 };
const SDL_Rect dstRect = { i * 64 + 24 + 7, 7, 16, 16 };
SDL_RenderCopy(&renderer, bmGUI.get(), &r, &dstRect);
}
}
}
//Draw a tool tip.
if(y<64 && x>=24 && x<24+m*64){
int i=(x-24)/64;
if (i + toolboxIndex < getEditorOrderMax()){
TexturePtr& tip = (!selectedLayer.empty())
? getCachedTextTexture(renderer, (i + toolboxIndex < (int)sceneryBlockNames.size())
? describeSceneryName(sceneryBlockNames[i + toolboxIndex]).c_str() : _("Custom scenery block"))
: typeTextTextures.at(editorTileOrder[i + toolboxIndex]);
const SDL_Rect tipSize = rectFromTexture(*tip);
SDL_Rect r = { 24 + i * 64, 64, 40, 40 };
if (r.x + tipSize.w>SCREEN_WIDTH - 50)
r.x = SCREEN_WIDTH - 50 - tipSize.w;
//Draw borders around text
Uint32 color = 0xFFFFFF00 | 230;
drawGUIBox(r.x - 2, r.y - 2, tipSize.w + 4, tipSize.h + 4, renderer, color);
//Draw tooltip's text
applyTexture(r.x, r.y, tip, renderer);
}
}
}else{
const SDL_Rect tbtSize = rectFromTexture(*toolboxText);
toolboxRect.x=SCREEN_WIDTH-tbtSize.w-28;
toolboxRect.y=0;
toolboxRect.w=tbtSize.w+28;
toolboxRect.h=tbtSize.h+4;
SDL_Rect r={SCREEN_WIDTH-tbtSize.w-24,2,16,16};
drawGUIBox(r.x-4,-2,tbtSize.w+32,tbtSize.h+6,renderer,0xFFFFFFFF);
//Draw "Toolbox" text.
applyTexture(r.x, r.y, toolboxText, renderer);
const SDL_Rect r2={96,0,16,16};
r.x=SCREEN_WIDTH-20;
r.w=r2.w;
r.h=r2.h;
// check if mouse is hovering on
if (x >= toolboxRect.x && x < toolboxRect.x + toolboxRect.w && y >= toolboxRect.y && y < toolboxRect.y + toolboxRect.h) {
tick = (tick + 1) & 31;
r.y += (tick < 16) ? (tick / 4 - 2) : (6 - tick / 4);
} else {
tick = 8;
}
//Draw arrow.
SDL_RenderCopy(&renderer, bmGUI.get(),&r2,&r);
}
}else{
toolboxRect.x=-1;
toolboxRect.y=-1;
toolboxRect.w=0;
toolboxRect.h=0;
}
//Draw a rectangle around the current tool.
Uint32 color=0xFFFFFF00;
drawGUIBox((SCREEN_WIDTH-440)/2+(tool*40)+(tool*10),SCREEN_HEIGHT-46,42,42,renderer,color);
}
void LevelEditor::showCurrentObject(SDL_Renderer& renderer){
//Get the current mouse location.
int x,y;
SDL_GetMouseState(&x,&y);
x+=camera.x;
y+=camera.y;
//Check if we should snap the block to grid or not.
if(!pressedShift){
snapToGrid(&x,&y);
}else{
x-=25;
y-=25;
}
//Check if the currentType is a legal type.
if(currentType>=0 && currentType<getEditorOrderMax()){
if (selectedLayer.empty()) {
// show normal blocks
ThemeBlock* obj = objThemes.getBlock(editorTileOrder[currentType]);
if (obj){
obj->editorPicture.draw(renderer, x - camera.x, y - camera.y);
}
} else {
// show scenery blocks
if (currentType < (int)sceneryBlockNames.size()) {
ThemeBlock* obj = objThemes.getScenery(sceneryBlockNames[currentType]);
if (obj){
obj->editorPicture.draw(renderer, x - camera.x, y - camera.y);
}
} else {
// it's custom scenery block
// just draw a stupid icon
const SDL_Rect r = { 48, 16, 16, 16 };
const SDL_Rect dstRect = { x - camera.x, y - camera.y, 16, 16 };
SDL_RenderCopy(&renderer, bmGUI.get(), &r, &dstRect);
}
}
}
}
void LevelEditor::determineNewPosition(int& x, int& y) {
if (dragCenter) {
SDL_Rect r = dragCenter->getBox();
x -= dragSrartPosition.x - r.x;
y -= dragSrartPosition.y - r.y;
} else {
x -= 25;
y -= 25;
}
// Check if we should snap the block to grid or not.
if (!pressedShift) {
x = int(floor(x/50.0f + 0.5f)) * 50;
y = int(floor(y/50.0f + 0.5f)) * 50;
}
}
void LevelEditor::determineNewSize(int x, int y, SDL_Rect& r) {
switch (selectionDrag % 3) {
case 0:
if (x > r.x + r.w - 15) x = r.x + r.w - 15;
if (!pressedShift) {
x = int(floor(x/50.0f + 0.5f)) * 50;
while (x > r.x + r.w - 15) x -= 50;
}
r.w += r.x - x;
r.x = x;
break;
case 2:
if (x < r.x + 15) x = r.x + 15;
if (!pressedShift) {
x = int(floor(x/50.0f + 0.5f)) * 50;
while (x < r.x + 15) x += 50;
}
r.w = x - r.x;
break;
}
switch (selectionDrag / 3) {
case 0:
if (y > r.y + r.h - 15) y = r.y + r.h - 15;
if (!pressedShift) {
y = int(floor(y/50.0f + 0.5f)) * 50;
while (y > r.y + r.h - 15) y -= 50;
}
r.h += r.y - y;
r.y = y;
break;
case 2:
if (y < r.y + 15) y = r.y + 15;
if (!pressedShift) {
y = int(floor(y/50.0f + 0.5f)) * 50;
while (y < r.y + 15) y += 50;
}
r.h = y - r.y;
break;
}
}
void LevelEditor::showSelectionDrag(SDL_Renderer& renderer){
//Check if the drag center isn't null.
if (dragCenter == NULL) return;
//Get the current mouse location.
int x,y;
SDL_GetMouseState(&x,&y);
//Create the rectangle.
x+=camera.x;
y+=camera.y;
//The location of the dragCenter.
SDL_Rect r = dragCenter->getBox();
if (selectionDrag == 4) { // dragging
// Check if we should snap the block to grid or not.
determineNewPosition(x, y);
//Loop through the selection.
//TODO: Check if block is in sight.
for (unsigned int o = 0; o < selection.size(); o++){
// FIXME: ad-hoc code which moves blocks temporarily, draw, and moves them back
const SDL_Rect r1 = selection[o]->getBox();
selection[o]->setBaseLocation((r1.x - r.x) + x, (r1.y - r.y) + y);
selection[o]->show(renderer);
selection[o]->setBaseLocation(r1.x, r1.y);
}
} else if (selectionDrag >= 0) { // resizing
// Check if we should snap the block to grid or not.
determineNewSize(x, y, r);
drawGUIBox(r.x - camera.x, r.y - camera.y, r.w, r.h, renderer, 0xFFFFFF33);
}
}
void LevelEditor::showConfigure(SDL_Renderer& renderer){
//arrow animation value. go through 0-65535 and loops.
static unsigned short arrowAnimation=0;
arrowAnimation++;
// skip if the Blocks layer is invisinble
if (!layerVisibility[std::string()]) return;
//Use theme color for arrows.
Uint32 color;
{
SDL_Color c = objThemes.getTextColor(false);
color = (Uint32(c.r) << 24) | (Uint32(c.g) << 16) | (Uint32(c.b) << 8) | 0xff;
}
//Draw the trigger lines.
{
map<Block*,vector<GameObject*> >::iterator it;
for(it=triggers.begin();it!=triggers.end();++it){
//Check if the trigger has linked targets.
if(!(*it).second.empty()){
//The location of the trigger.
SDL_Rect r=(*it).first->getBox();
//Loop through the targets.
for(unsigned int o=0;o<(*it).second.size();o++){
//Get the location of the target.
SDL_Rect r1=(*it).second[o]->getBox();
//Draw the line from the center of the trigger to the center of the target.
drawLineWithArrow(r.x-camera.x+25,r.y-camera.y+25,r1.x-camera.x+25,r1.y-camera.y+25,renderer,color,32,arrowAnimation%32);
//Also draw two selection marks.
applyTexture(r.x-camera.x+25-2,r.y-camera.y+25-2,selectionMark,renderer);
applyTexture(r1.x-camera.x+25-2,r1.y-camera.y+25-2,selectionMark,renderer);
}
}
}
//Draw a line to the mouse from the linkingTrigger when linking.
if(linking){
//Get the current mouse location.
int x,y;
SDL_GetMouseState(&x,&y);
//Draw the line from the center of the trigger to mouse.
drawLineWithArrow(linkingTrigger->getBox().x-camera.x+25,linkingTrigger->getBox().y-camera.y+25,x,y,renderer,color,32,arrowAnimation%32);
}
}
//This saves the stacked pause marks on each position.
std::map<std::pair<int, int>, int> stackedMarks;
//Draw the moving positions.
map<Block*,vector<MovingPosition> >::iterator it;
for(it=movingBlocks.begin();it!=movingBlocks.end();++it){
//Check if the block has positions.
if(!(*it).second.empty()){
//The location of the moving block.
SDL_Rect block=(*it).first->getBox();
block.x+=25-camera.x;
block.y+=25-camera.y;
//The location of the previous position.
//The first time it's the moving block's position self.
SDL_Rect r=block;
//Loop through the positions.
for(unsigned int o=0;o<(*it).second.size();o++){
//Draw the line from the center of the previous position to the center of the position.
//x and y are the coordinates for the current moving position.
int x=block.x+(*it).second[o].x;
int y=block.y+(*it).second[o].y;
//Check if we need to draw line
double dx=r.x-x;
double dy=r.y-y;
double d=sqrt(dx*dx+dy*dy);
if(d>0.001f){
if(it->second[o].time>0){
//Calculate offset to contain the moving speed.
int offset=int(d*arrowAnimation/it->second[o].time)%32;
drawLineWithArrow(r.x,r.y,x,y,renderer,color,32,offset);
}else{
//time==0 ???? so don't draw arrow at all
drawLine(r.x,r.y,x,y,renderer);
}
} else {
// distance==0 which means pause mode
// FIXME: it's ugly
SDL_Rect r1 = { 0, 0, 16, 16 }, r2 = { x - 25, y - 25 + 15 * (stackedMarks[std::pair<int, int>(x, y)]++), 16, 16 };
if (it->second[o].time) {
char s[64];
sprintf(s, "%gs", float(it->second[o].time) * 0.025f);
r1.x = 0; r1.y = 80;
SDL_RenderCopy(&renderer, bmGUI.get(), &r1, &r2);
r2.x += 16;
for (int i = 0; s[i]; i++) {
if (s[i] >= '0' && s[i] <= '9') {
r1.x = 16 + (s[i] - '0') * 8;
r1.w = 8;
} else if (s[i] == '.') {
r1.x = 96;
r1.w = 8;
} else if (s[i] == 's') {
r1.x = 104;
r1.w = 8;
} else {
// show some garbage
r1.x = 0;
r1.w = 1;
}
r2.w = 8;
SDL_RenderCopy(&renderer, bmGUI.get(), &r1, &r2);
r2.x += 8;
}
} else {
r1.x = 32; r1.y = 64;
SDL_RenderCopy(&renderer, bmGUI.get(), &r1, &r2);
}
}
//And draw a marker at the end.
applyTexture(x-13,y-13,movingMark,renderer);
//Get the box of the previous position.
SDL_Rect tmp={x,y,0,0};
r=tmp;
}
}
}
//Draw a line to the mouse from the previous moving pos.
if(moving){
//Get the current mouse location.
int x,y;
SDL_GetMouseState(&x,&y);
//Check if we should snap the block to grid or not.
if(!pressedShift){
x+=camera.x;
y+=camera.y;
snapToGrid(&x,&y);
x-=camera.x;
y-=camera.y;
}else{
x-=25;
y-=25;
}
int posX,posY;
//Check if there are moving positions for the moving block.
if(!movingBlocks[movingBlock].empty()){
//Draw the line from the center of the previouse moving positions to mouse.
posX=movingBlocks[movingBlock].back().x;
posY=movingBlocks[movingBlock].back().y;
posX-=camera.x;
posY-=camera.y;
posX+=movingBlock->getBox().x;
posY+=movingBlock->getBox().y;
}else{
//Draw the line from the center of the movingblock to mouse.
posX=movingBlock->getBox().x-camera.x;
posY=movingBlock->getBox().y-camera.y;
}
//Check if the current point is the same as the previous point
if (posX == x && posY == y) {
pauseMode = true;
} else {
pauseMode = false;
//Calculate offset to contain the moving speed.
int offset = int(double(arrowAnimation)*movingSpeed / 10.0) % 32;
//Draw the line.
drawLineWithArrow(posX + 25, posY + 25, x + 25, y + 25, renderer, color, 32, offset);
}
//Draw a marker.
applyTexture(x+12,y+12,movingMark,renderer);
}
}
void LevelEditor::resize(ImageManager &imageManager, SDL_Renderer &renderer){
//Call the resize method of the Game.
Game::resize(imageManager, renderer);
//Move the toolbar's position rect used for collision.
toolbarRect.x=(SCREEN_WIDTH-460)/2;
toolbarRect.y=SCREEN_HEIGHT-50;
}
//Filling the order array
const int LevelEditor::editorTileOrder[EDITOR_ORDER_MAX]={
TYPE_BLOCK,
TYPE_SHADOW_BLOCK,
TYPE_SPIKES,
TYPE_FRAGILE,
TYPE_MOVING_BLOCK,
TYPE_MOVING_SHADOW_BLOCK,
TYPE_MOVING_SPIKES,
TYPE_CONVEYOR_BELT,
TYPE_SHADOW_CONVEYOR_BELT,
TYPE_BUTTON,
TYPE_SWITCH,
TYPE_PORTAL,
TYPE_SWAP,
TYPE_CHECKPOINT,
TYPE_NOTIFICATION_BLOCK,
TYPE_START_PLAYER,
TYPE_START_SHADOW,
TYPE_EXIT,
TYPE_COLLECTABLE,
TYPE_PUSHABLE
};
diff --git a/src/LevelPlaySelect.cpp b/src/LevelPlaySelect.cpp
index cfae54a..81fb401 100644
--- a/src/LevelPlaySelect.cpp
+++ b/src/LevelPlaySelect.cpp
@@ -1,503 +1,500 @@
/*
* Copyright (C) 2011-2013 Me and My Shadow
*
* This file is part of Me and My Shadow.
*
* Me and My Shadow is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Me and My Shadow is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Me and My Shadow. If not, see <http://www.gnu.org/licenses/>.
*/
#include "LevelPlaySelect.h"
#include "GameState.h"
#include "Functions.h"
#include "FileManager.h"
#include "Globals.h"
#include "LevelSelect.h"
#include "GUIObject.h"
#include "GUIListBox.h"
#include "GUIScrollBar.h"
#include "InputManager.h"
#include "ThemeManager.h"
#include "SoundManager.h"
#include "StatisticsManager.h"
#include "Game.h"
#include <stdio.h>
#include <string>
#include <sstream>
#include <iostream>
+#include "libs/tinyformat/tinyformat.h"
+
/////////////////////LEVEL SELECT/////////////////////
LevelPlaySelect::LevelPlaySelect(ImageManager& imageManager, SDL_Renderer& renderer)
:LevelSelect(imageManager,renderer,_("Select Level")),
levelInfoRender(imageManager,renderer,getDataPath(),*fontText,objThemes.getTextColor(false)){
//Load the play button if needed.
playButtonImage=imageManager.loadTexture(getDataPath()+"gfx/playbutton.png", renderer);
//Create the gui.
createGUI(imageManager,renderer, true);
//Show level list
refresh(imageManager,renderer);
}
LevelPlaySelect::~LevelPlaySelect(){
play=NULL;
//Clear the selected level.
if(selectedNumber!=NULL){
delete selectedNumber;
selectedNumber=NULL;
}
}
void LevelPlaySelect::createGUI(ImageManager& imageManager,SDL_Renderer &renderer, bool initial){
//Create the play button.
if(initial){
play=new GUIButton(imageManager,renderer,SCREEN_WIDTH-240,SCREEN_HEIGHT-60,240,32,_("Play"));
}else{
play->left=SCREEN_WIDTH-240;
play->top=SCREEN_HEIGHT-60;
}
play->name="cmdPlay";
play->eventCallback=this;
play->enabled=false;
if(initial)
GUIObjectRoot->addChild(play);
}
void LevelPlaySelect::refresh(ImageManager& imageManager, SDL_Renderer& renderer, bool /*change*/){
const int m=levels->getLevelCount();
numbers.clear();
levelInfoRender.resetText(renderer, *fontText, objThemes.getTextColor(false));
//Create the non selected number.
if (selectedNumber == NULL){
selectedNumber = new Number(imageManager, renderer);
}
SDL_Rect box={40,SCREEN_HEIGHT-130,50,50};
selectedNumber->init(renderer," ",box);
selectedNumber->setLocked(true);
selectedNumber->setMedal(0);
bestTimeFilePath.clear();
bestRecordingFilePath.clear();
//Disable the play button.
play->enabled=false;
for(int n=0; n<m; n++){
numbers.emplace_back(imageManager, renderer);
}
for(int n=0; n<m; n++){
SDL_Rect box={(n%LEVELS_PER_ROW)*64+static_cast<int>(SCREEN_WIDTH*0.2)/2,(n/LEVELS_PER_ROW)*64+184,0,0};
numbers[n].init(renderer,n,box);
numbers[n].setLocked(n>0 && levels->getLocked(n));
int medal=levels->getLevel(n)->won?1:0;
if(medal){
const int targetTime = levels->getLevel(n)->targetTime;
const int targetRecordings = levels->getLevel(n)->targetRecordings;
const int time = levels->getLevel(n)->time;
const int recordings = levels->getLevel(n)->recordings;
if (time >= 0 && (targetTime < 0 || time <= targetTime))
medal++;
if (recordings >= 0 && (targetRecordings < 0 || recordings <= targetRecordings))
medal++;
}
numbers[n].setMedal(medal);
}
if(m>LEVELS_DISPLAYED_IN_SCREEN){
levelScrollBar->maxValue=(m-LEVELS_DISPLAYED_IN_SCREEN+(LEVELS_PER_ROW-1))/LEVELS_PER_ROW;
levelScrollBar->visible=true;
}else{
levelScrollBar->maxValue=0;
levelScrollBar->visible=false;
}
if (levels->levelpackPath == LEVELS_PATH || levels->levelpackPath == CUSTOM_LEVELS_PATH)
levelpackDescription->caption = _("Individual levels which are not contained in any level packs");
else if (!levels->levelpackDescription.empty())
levelpackDescription->caption = _CC(levels->getDictionaryManager(), levels->levelpackDescription);
else
levelpackDescription->caption = "";
}
void LevelPlaySelect::selectNumber(ImageManager& imageManager, SDL_Renderer& renderer, unsigned int number,bool selected){
if (selected) {
if (number >= 0 && number < levels->getLevelCount()) {
levels->setCurrentLevel(number);
setNextState(STATE_GAME);
}
}else{
displayLevelInfo(imageManager, renderer,number);
}
}
void LevelPlaySelect::checkMouse(ImageManager &imageManager, SDL_Renderer &renderer){
int x,y;
//Get the current mouse location.
SDL_GetMouseState(&x,&y);
//Check if we should replay the record.
if(selectedNumber!=NULL){
SDL_Rect mouse={x,y,0,0};
if(!bestTimeFilePath.empty()){
SDL_Rect box={SCREEN_WIDTH-420,SCREEN_HEIGHT-130,372,32};
if(pointOnRect(mouse, box)){
Game::recordFile=bestTimeFilePath;
levels->setCurrentLevel(selectedNumber->getNumber());
setNextState(STATE_GAME);
return;
}
}
if(!bestRecordingFilePath.empty()){
SDL_Rect box={SCREEN_WIDTH-420,SCREEN_HEIGHT-98,372,32};
if(pointOnRect(mouse, box)){
Game::recordFile=bestRecordingFilePath;
levels->setCurrentLevel(selectedNumber->getNumber());
setNextState(STATE_GAME);
return;
}
}
}
//Call the base method from the super class.
LevelSelect::checkMouse(imageManager, renderer);
}
void LevelPlaySelect::displayLevelInfo(ImageManager& imageManager, SDL_Renderer& renderer, int number){
//Update currently selected level
if(selectedNumber==NULL){
selectedNumber=new Number(imageManager, renderer);
}
SDL_Rect box={40,SCREEN_HEIGHT-130,50,50};
if (number >= 0 && number < levels->getLevelCount()) {
selectedNumber->init(renderer, number, box);
selectedNumber->setLocked(false);
//Show level medal
int medal = levels->getLevel(number)->won ? 1 : 0;
int time = levels->getLevel(number)->time;
int targetTime = levels->getLevel(number)->targetTime;
int recordings = levels->getLevel(number)->recordings;
int targetRecordings = levels->getLevel(number)->targetRecordings;
if (medal){
if (time >= 0 && (targetTime < 0 || time <= targetTime))
medal++;
if (recordings >= 0 && (targetRecordings < 0 || recordings <= targetRecordings))
medal++;
}
selectedNumber->setMedal(medal);
std::string levelTime;
std::string levelRecs;
//Show best time and recordings
if (medal){
- char s[64];
-
if (time >= 0)
- if (targetTime>=0)
- sprintf(s, "%-.2fs / %-.2fs", time / 40.0, targetTime / 40.0);
+ if (targetTime >= 0)
+ levelTime = tfm::format("%-.2fs / %-.2fs", time / 40.0, targetTime / 40.0);
else
- sprintf(s, "%-.2fs / -", time / 40.0);
+ levelTime = tfm::format("%-.2fs / -", time / 40.0);
else
- s[0] = '\0';
- levelTime = s;
+ levelTime.clear();
if (recordings >= 0)
if (targetRecordings >= 0)
- sprintf(s, "%5d / %d", recordings, targetRecordings);
+ levelRecs = tfm::format("%5d / %d", recordings, targetRecordings);
else
- sprintf(s, "%5d / -", recordings);
+ levelRecs = tfm::format("%5d / -", recordings);
else
- s[0] = '\0';
- levelRecs = s;
+ levelRecs.clear();
} else{
levelTime = "- / -";
levelRecs = "- / -";
}
//Show the play button.
play->enabled = true;
//Check if there is auto record file
levels->getLevelAutoSaveRecordPath(number, bestTimeFilePath, bestRecordingFilePath, false);
if (!bestTimeFilePath.empty()){
FILE *f;
f = fopen(bestTimeFilePath.c_str(), "rb");
if (f == NULL){
bestTimeFilePath.clear();
} else{
fclose(f);
}
}
if (!bestRecordingFilePath.empty()){
FILE *f;
f = fopen(bestRecordingFilePath.c_str(), "rb");
if (f == NULL){
bestRecordingFilePath.clear();
} else{
fclose(f);
}
}
//Show level description
levelInfoRender.update(renderer, *fontText, objThemes.getTextColor(false),
_CC(levels->getDictionaryManager(), levels->getLevelName(number)), levelTime, levelRecs);
} else {
levelInfoRender.resetText(renderer, *fontText, objThemes.getTextColor(false));
selectedNumber->init(renderer, " ", box);
selectedNumber->setLocked(true);
selectedNumber->setMedal(0);
bestTimeFilePath.clear();
bestRecordingFilePath.clear();
//Disable the play button.
play->enabled = false;
}
}
void LevelPlaySelect::handleEvents(ImageManager& imageManager, SDL_Renderer& renderer){
//Call handleEvents() of base class.
LevelSelect::handleEvents(imageManager, renderer);
//Check if the cheat code is input which is used to skip locked level.
//NOTE: The cheat code is NOT in plain text, since we don't want you to find it out immediately.
//NOTE: If you type it wrong, please press a key which is NOT a-z before retype it (as the code suggests).
if (event.type == SDL_KEYDOWN) {
static Uint32 hash = 0;
if (event.key.keysym.sym >= SDLK_a && event.key.keysym.sym <= SDLK_z) {
Uint32 c = event.key.keysym.sym - SDLK_a + 1;
hash = hash * 1296096U + c;
if (hash == 498506457U) {
if (selectedNumber) {
int n = selectedNumber->getNumber();
if (n >= 0 && n < (int)numbers.size() - 1 && numbers[n + 1].getLocked()) {
//unlock the level temporarily
numbers[n + 1].setLocked(false);
//play a sound effect
getSoundManager()->playSound("hit");
//new achievement
statsMgr.newAchievement("cheat");
}
}
hash = 0;
}
} else {
hash = 0;
}
}
if (section == 3) {
//Check focus movement
if (inputMgr.isKeyDownEvent(INPUTMGR_DOWN) || inputMgr.isKeyDownEvent(INPUTMGR_RIGHT)){
isKeyboardOnly = true;
section2++;
} else if (inputMgr.isKeyDownEvent(INPUTMGR_UP) || inputMgr.isKeyDownEvent(INPUTMGR_LEFT)){
isKeyboardOnly = true;
section2--;
}
if (section2 > 3) section2 = 1;
else if (section2 < 1) section2 = 3;
//Check if enter is pressed
if (isKeyboardOnly && inputMgr.isKeyDownEvent(INPUTMGR_SELECT) && selectedNumber) {
int n = selectedNumber->getNumber();
if (n >= 0) {
switch (section2) {
case 1:
if (!bestTimeFilePath.empty()) {
Game::recordFile = bestTimeFilePath;
levels->setCurrentLevel(n);
setNextState(STATE_GAME);
}
break;
case 2:
if (!bestRecordingFilePath.empty()) {
Game::recordFile = bestRecordingFilePath;
levels->setCurrentLevel(n);
setNextState(STATE_GAME);
}
break;
case 3:
selectNumber(imageManager, renderer, n, true);
break;
}
}
}
}
}
void LevelPlaySelect::render(ImageManager& imageManager, SDL_Renderer &renderer){
//First let the levelselect render.
LevelSelect::render(imageManager,renderer);
int x,y,dy=0;
//Get the current mouse location.
SDL_GetMouseState(&x,&y);
if(levelScrollBar)
dy=levelScrollBar->value;
//Upper bound of levels we'd like to display.
y+=dy*64;
SDL_Rect mouse={x,y,0,0};
//Show currently selected level (if any)
if(selectedNumber!=NULL){
selectedNumber->show(renderer, 0);
//Only show the replay button if the level is completed (won).
if(selectedNumber->getNumber()>=0 && selectedNumber->getNumber()<levels->getLevelCount()) {
if(levels->getLevel(selectedNumber->getNumber())->won){
if(!bestTimeFilePath.empty()){
SDL_Rect r={0,0,32,32};
const SDL_Rect box={SCREEN_WIDTH-420,SCREEN_HEIGHT-130,372,32};
if (isKeyboardOnly ? (section == 3 && section2 == 1) : pointOnRect(mouse, box)){
r.x = 32;
drawGUIBox(box.x, box.y, box.w, box.h, renderer, 0xFFFFFF40);
}
const SDL_Rect dstRect = {SCREEN_WIDTH-80,SCREEN_HEIGHT-130,r.w,r.h};
SDL_RenderCopy(&renderer,playButtonImage.get(),&r, &dstRect);
}
if(!bestRecordingFilePath.empty()){
SDL_Rect r={0,0,32,32};
const SDL_Rect box={SCREEN_WIDTH-420,SCREEN_HEIGHT-98,372,32};
if (isKeyboardOnly ? (section == 3 && section2 == 2) : pointOnRect(mouse, box)){
r.x = 32;
drawGUIBox(box.x, box.y, box.w, box.h, renderer, 0xFFFFFF40);
}
const SDL_Rect dstRect = {SCREEN_WIDTH-80,SCREEN_HEIGHT-98,r.w,r.h};
SDL_RenderCopy(&renderer,playButtonImage.get(),&r, &dstRect);
}
}
}
levelInfoRender.render(renderer);
}
//Draw highlight for play button.
if (isKeyboardOnly && play && play->enabled) {
play->state = (section == 3 && section2 == 3) ? 1 : 0;
}
}
void LevelPlaySelect::renderTooltip(SDL_Renderer &renderer, unsigned int number, int dy){
if (!toolTip.name || toolTip.number != number) {
- const int SLEN = 64;
- char s[SLEN];
-
//Render the name of the level.
toolTip.name = textureFromText(renderer, *fontText, _CC(levels->getDictionaryManager(), levels->getLevelName(number)), objThemes.getTextColor(true));
toolTip.time=nullptr;
toolTip.recordings=nullptr;
toolTip.number=number;
//The time it took.
if(levels->getLevel(number)->time>0){
- SDL_snprintf(s,SLEN,"%-.2fs",levels->getLevel(number)->time/40.0);
- toolTip.time = textureFromText(renderer, *fontText, s, objThemes.getTextColor(true));
+ toolTip.time = textureFromText(renderer, *fontText,
+ tfm::format("%-.2fs", levels->getLevel(number)->time / 40.0).c_str(),
+ objThemes.getTextColor(true));
}
//The number of recordings it took.
if(levels->getLevel(number)->recordings>=0){
- SDL_snprintf(s,SLEN,"%d",levels->getLevel(number)->recordings);
- toolTip.recordings = textureFromText(renderer, *fontText, s, objThemes.getTextColor(true));
+ toolTip.recordings = textureFromText(renderer, *fontText,
+ tfm::format("%d", levels->getLevel(number)->recordings).c_str(),
+ objThemes.getTextColor(true));
}
}
const SDL_Rect nameSize = rectFromTexture(*toolTip.name);
//Now draw a square the size of the three texts combined.
SDL_Rect r=numbers[number].box;
r.y-=dy*64;
if(toolTip.time && toolTip.recordings){
const int recW = textureWidth(*toolTip.recordings);
const int timeW = textureWidth(*toolTip.time);
r.w=(nameSize.w)>(25+timeW+40+recW)?(nameSize.w):(25+timeW+40+recW);
r.h=nameSize.h+5+20;
}else{
r.w=nameSize.w;
r.h=nameSize.h;
}
//Make sure the tooltip doesn't go outside the window.
if(r.y>SCREEN_HEIGHT-200){
r.y-=nameSize.h+4;
}else{
r.y+=numbers[number].box.h+2;
}
if(r.x+r.w>SCREEN_WIDTH-50)
r.x=SCREEN_WIDTH-50-r.w;
//Draw a rectange
Uint32 color=0xFFFFFFFF;
drawGUIBox(r.x-5,r.y-5,r.w+10,r.h+10,renderer,color);
//Calc the position to draw.
SDL_Rect r2=r;
//Now we render the name if the surface isn't null.
if(toolTip.name){
//Draw the name.
applyTexture(r2.x, r2.y, toolTip.name, renderer);
}
//Increase the height to leave a gap between name and stats.
r2.y+=30;
if(toolTip.time){
//Now draw the time.
applyTexture(r2.x,r2.y,levelInfoRender.timeIcon,renderer);
r2.x+=25;
applyTexture(r2.x, r2.y, toolTip.time, renderer);
r2.x+=textureWidth(*toolTip.time)+15;
}
if(toolTip.recordings){
//Now draw the recordings.
applyTexture(r2.x,r2.y,levelInfoRender.recordingsIcon,renderer);
r2.x+=25;
applyTexture(r2.x, r2.y, toolTip.recordings, renderer);
}
}
void LevelPlaySelect::resize(ImageManager &imageManager, SDL_Renderer &renderer){
//Let the LevelSelect do his stuff.
LevelSelect::resize(imageManager, renderer);
//Now create our gui again.
createGUI(imageManager,renderer, false);
}
void LevelPlaySelect::GUIEventCallback_OnEvent(ImageManager& imageManager, SDL_Renderer& renderer, std::string name,GUIObject* obj,int eventType){
//Let the level select handle his GUI events.
LevelSelect::GUIEventCallback_OnEvent(imageManager,renderer,name,obj,eventType);
//Check for the play button.
if(name=="cmdPlay"){
if(selectedNumber!=NULL){
levels->setCurrentLevel(selectedNumber->getNumber());
setNextState(STATE_GAME);
}
}
}
diff --git a/src/Main.cpp b/src/Main.cpp
index 8fd4cc0..6c8db3a 100644
--- a/src/Main.cpp
+++ b/src/Main.cpp
@@ -1,248 +1,249 @@
/*
* Copyright (C) 2011-2013 Me and My Shadow
*
* This file is part of Me and My Shadow.
*
* Me and My Shadow is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Me and My Shadow is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Me and My Shadow. If not, see <http://www.gnu.org/licenses/>.
*/
#include "Functions.h"
#include "Globals.h"
#include "TitleMenu.h"
#include "GUIObject.h"
#include "InputManager.h"
#include "StatisticsManager.h"
#include "Timer.h"
#include <SDL.h>
#include <stdlib.h>
#include <time.h>
#include <stdio.h>
#include <string.h>
// the following piece of code fixes the combination of VC2015 with official SDL1.2 binary
#if defined(_MSC_VER) && (_MSC_VER >= 1900)
#pragma comment(lib, "legacy_stdio_definitions.lib")
FILE _iob[] = {*stdin, *stdout, *stderr};
extern "C" FILE * __cdecl __iob_func(void)
{
return _iob;
}
#endif
int main(int argc, char** argv) {
#ifdef _MSC_VER
//Fix the non-latin file name bug under Visual Studio
setlocale(LC_ALL,"");
+ setlocale(LC_NUMERIC, "C");
#endif
//Relocate the standard output for debug purpose (?)
#if defined(ANDROID)
freopen("stdout.txt","w",stdout);
freopen("stderr.txt","w",stderr);
#endif
//First parse the comand line arguments.
int s=parseArguments(argc,argv);
if(s==-1){
printf("Usage: %s [OPTIONS] ...\n",argv[0]);
printf("%s","Available options:\n");
printf(" %-5s%-30s %s\n","","--data-dir <dir>","Specifies the data directory.");
printf(" %-5s%-30s %s\n","","--user-dir <dir>","Specifies the user preferences directory.");
printf(" %-5s%-30s %s\n","-f,","--fullscreen","Run the game fullscreen.");
printf(" %-5s%-30s %s\n","-w,","--windowed","Run the game windowed.");
printf(" %-5s%-30s %s\n","-mv,","--music <volume>","Set the music volume.");
printf(" %-5s%-30s %s\n","-sv,","--sound <volume>","Set the sound volume.");
printf(" %-5s%-30s %s\n","-s,","--set <setting> <value>","Change a setting to a given value.");
printf(" %-5s%-30s %s\n","-v,","--version","Display the version and quit.");
printf(" %-5s%-30s %s\n","-h,","--help","Display this help message.");
return 0;
}else if(s==0){
return 0;
}
//Try to configure the dataPath, userPath, etc...
if(configurePaths()==false){
fprintf(stderr,"FATAL ERROR: Failed to configure paths.\n");
return 1;
}
//Load the settings.
if(loadSettings()==false){
fprintf(stderr,"ERROR: Unable to load config file, default values will be used.\n");
}
ScreenData screenData = init();
//Initialise some stuff like SDL, the window, SDL_Mixer.
if(!screenData) {
fprintf(stderr,"FATAL ERROR: Failed to initialize game.\n");
return 1;
}
SDL_Renderer& renderer = *screenData.renderer;
//Initialise the imagemanager.
//The ImageManager is used to prevent loading images multiple times.
ImageManager imageManager;
//Load some important files like the background music, default theme.
if(loadFiles(imageManager,renderer)==false){
fprintf(stderr,"FATAL ERROR: Failed to load necessary files.\n");
return 1;
}
//Seed random.
srand((unsigned)time(NULL));
//Set the currentState id to the main menu and create it.
stateID=STATE_MENU;
currentState=new Menu(imageManager,renderer);
//Set the fadeIn value to zero.
int fadeIn=255;
//Keep the last resize event, this is to only process one.
SDL_Event lastResize={};
Timer timer;
//Start the game loop.
while(stateID!=STATE_EXIT){
//We start the timer.
timer.start();
//Loop the SDL events.
while(SDL_PollEvent(&event)){
//Check if user resizes the window.
if(event.type==SDL_WINDOWEVENT){
if(event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED)
{
lastResize=event;
//Don't let other objects process this event (?)
continue;
}
}
//Check if it's mouse event. If it's true then we quit the keyboard-only mode.
if (event.type == SDL_MOUSEMOTION || event.type == SDL_MOUSEBUTTONDOWN || event.type == SDL_MOUSEBUTTONUP) {
isKeyboardOnly = false;
}
//Check if the fullscreen toggle shortcut is pressed (Alt+Enter).
if(event.type==SDL_KEYDOWN && event.key.keysym.sym==SDLK_RETURN && (event.key.keysym.mod & KMOD_ALT)){
getSettings()->setValue("fullscreen",getSettings()->getBoolValue("fullscreen")?"0":"1");
//We need to create a new screen.
if(!createScreen()){
//Screen creation failed so set to safe settings.
getSettings()->setValue("fullscreen","0");
getSettings()->setValue("width","800");
getSettings()->setValue("height","600");
//Try it with the safe settings.
if(!createScreen()){
//Everything fails so quit.
setNextState(STATE_EXIT);
std::cerr<<"ERROR: Unable to create screen."<<std::endl;
}
}
//The screen is created, now load the (menu) theme.
if(!loadTheme(imageManager,renderer,"")){
//Loading the theme failed so quit.
setNextState(STATE_EXIT);
std::cerr<<"ERROR: Unable to load theme after toggling fullscreen."<<std::endl;
}
//Don't let other objects process this event.
continue;
}
//Set the cursor type to the default one, the GUI can change that if needed.
currentCursor=CURSOR_POINTER;
//Let the input manager handle the events.
inputMgr.updateState(true);
//Let the currentState handle the events.
currentState->handleEvents(imageManager,renderer);
//Also pass the events to the GUI.
GUIObjectHandleEvents(imageManager,renderer);
}
//Process the resize event.
if(lastResize.type==SDL_WINDOWEVENT){
//TODO - used to be SDL_VIDEORESIZE
// so this may trigger on more events than intended
event=lastResize;
onVideoResize(imageManager,renderer);
//After resize we erase the event type
//TODO - used to be SDL_NOEVENT
lastResize.type=SDL_FIRSTEVENT;
}
//maybe we should add a check here (??) to fix some bugs (ticket #47)
if(nextState!=STATE_NULL){
changeState(imageManager, renderer, fadeIn);
//Check if fading is enabled.
if(getSettings()->getBoolValue("fading"))
fadeIn=17;
else
fadeIn=255;
}
if(stateID==STATE_EXIT) break;
//update input state (??)
inputMgr.updateState(false);
//Now it's time for the state to do his logic.
currentState->logic(imageManager,renderer);
currentState->render(imageManager,renderer);
//TODO: Shouldn't the gamestate take care of rendering the GUI?
if(GUIObjectRoot) GUIObjectRoot->render(renderer);
//draw fading effect
if(fadeIn>0&&fadeIn<255){
dimScreen(renderer, static_cast<Uint8>(255-fadeIn));
fadeIn+=17;
}
//draw new achievements (if any) as overlay
statsMgr.render(imageManager, renderer);
//Before flipping the screen set the cursor.
SDL_SetCursor(cursors[currentCursor]);
//And draw the screen surface to the actual screen.
flipScreen(renderer);
//Now calcualte how long we need to wait to keep a constant framerate.
int t=timer.getTicks();
t=(1000/FPS)-t;
if(t>0){
SDL_Delay(t);
}
}
//The game has ended, save the settings just to be sure.
if(!saveSettings()){
fprintf(stderr,"ERROR: Unable to save settings in config file.\n");
}
//Clean everything up.
clean();
//End of program.
return 0;
}
diff --git a/src/OptionsMenu.cpp b/src/OptionsMenu.cpp
index f0b4057..bf834c8 100644
--- a/src/OptionsMenu.cpp
+++ b/src/OptionsMenu.cpp
@@ -1,592 +1,610 @@
/*
* Copyright (C) 2011-2013 Me and My Shadow
*
* This file is part of Me and My Shadow.
*
* Me and My Shadow is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Me and My Shadow is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Me and My Shadow. If not, see <http://www.gnu.org/licenses/>.
*/
#include "Functions.h"
#include "GameState.h"
#include "OptionsMenu.h"
#include "ThemeManager.h"
#include "GUIListBox.h"
#include "GUISlider.h"
#include "InputManager.h"
#include "LevelPackManager.h"
#include "StatisticsManager.h"
#include "MusicManager.h"
#include "SoundManager.h"
#include <iostream>
#include <sstream>
+#include "libs/tinyformat/tinyformat.h"
#include "libs/tinygettext/tinygettext.hpp"
#include "libs/findlocale/findlocale.h"
using namespace std;
/////////////////////////OPTIONS_MENU//////////////////////////////////
//Some variables for the options.
static bool fullscreen,internet,fade,quickrec;
static string themeName,languageName;
static int lastLang,lastRes;
static bool useProxy;
static string internetProxy;
static bool restartFlag;
-static _res currentRes;
-static vector<_res> resolutionList;
+static SDL_Point currentRes;
+static vector<SDL_Point> resolutionList;
Options::Options(ImageManager& imageManager,SDL_Renderer& renderer){
//Render the title.
title = titleTextureFromText(renderer, _("Settings"), objThemes.getTextColor(false), SCREEN_WIDTH);
//Initialize variables.
lastJumpSound=0;
clearIconHower=false;
section = 2;
section2 = 1;
//Load icon image and tooltip text.
clearIcon=imageManager.loadTexture(getDataPath()+"gfx/menu/clear-progress.png",renderer);
/// TRANSLATORS: Used for button which clear any level progress like unlocked levels and highscores.
clearTooltip=textureFromText(renderer, *fontText, _("Clear Progress"), objThemes.getTextColor(true));
//Set some default settings.
fullscreen=getSettings()->getBoolValue("fullscreen");
languageName=getSettings()->getValue("lang");
themeName=processFileName(getSettings()->getValue("theme"));
internet=getSettings()->getBoolValue("internet");
internetProxy=getSettings()->getValue("internet-proxy");
useProxy=!internetProxy.empty();
fade=getSettings()->getBoolValue("fading");
quickrec=getSettings()->getBoolValue("quickrecord");
//Set the restartFlag false.
restartFlag=false;
//Now create the gui.
createGUI(imageManager,renderer);
}
Options::~Options(){
//Delete the GUI.
if(GUIObjectRoot){
delete GUIObjectRoot;
GUIObjectRoot=NULL;
}
}
void Options::createGUI(ImageManager& imageManager,SDL_Renderer& renderer){
//Variables for positioning
const int columnW=SCREEN_WIDTH*0.3;
const int column1X=SCREEN_WIDTH*0.15;
const int column2X=SCREEN_WIDTH*0.55;
const int lineHeight=40;
//Create the root element of the GUI.
if(GUIObjectRoot){
delete GUIObjectRoot;
GUIObjectRoot=NULL;
}
GUIObjectRoot=new GUIObject(imageManager,renderer,0,0,SCREEN_WIDTH,SCREEN_HEIGHT);
//Single line list for different tabs.
GUISingleLineListBox* listBox=new GUISingleLineListBox(imageManager,renderer,(SCREEN_WIDTH-500)/2,104,500,32);
listBox->addItem(_("General"));
listBox->addItem(_("Controls"));
listBox->value=0;
listBox->name="lstTabs";
listBox->eventCallback=this;
GUIObjectRoot->addChild(listBox);
//Create general tab.
tabGeneral=new GUIObject(imageManager,renderer,0,150,SCREEN_WIDTH,SCREEN_HEIGHT);
GUIObjectRoot->addChild(tabGeneral);
//Now we create GUIObjects for every option.
GUIObject* obj=new GUILabel(imageManager,renderer,column1X,0,columnW,36,_("Music"));
tabGeneral->addChild(obj);
musicSlider=new GUISlider(imageManager,renderer,column2X,0,columnW,36,atoi(getSettings()->getValue("music").c_str()),0,128,15);
musicSlider->name="sldMusic";
musicSlider->eventCallback=this;
tabGeneral->addChild(musicSlider);
obj=new GUILabel(imageManager,renderer,column1X,lineHeight,columnW,36,_("Sound"));
tabGeneral->addChild(obj);
soundSlider=new GUISlider(imageManager,renderer,column2X,lineHeight,columnW,36,atoi(getSettings()->getValue("sound").c_str()),0,128,15);
soundSlider->name="sldSound";
soundSlider->eventCallback=this;
tabGeneral->addChild(soundSlider);
obj=new GUILabel(imageManager,renderer,column1X,2*lineHeight,columnW,36,_("Resolution"));
obj->name="lstResolution";
tabGeneral->addChild(obj);
//Create list with many different resolutions.
resolutions = new GUISingleLineListBox(imageManager,renderer,column2X,2*lineHeight,columnW,36);
resolutions->value=-1;
//Only get the resolution list if it hasn't been done before.
if(resolutionList.empty()){
resolutionList=getResolutionList();
}
//Get current resolution from config file. Thus it can be user defined.
- currentRes.w=atoi(getSettings()->getValue("width").c_str());
- currentRes.h=atoi(getSettings()->getValue("height").c_str());
+ currentRes.x=atoi(getSettings()->getValue("width").c_str());
+ currentRes.y=atoi(getSettings()->getValue("height").c_str());
for(int i=0; i<(int)resolutionList.size();i++){
//Create a string from width and height and then add it to list.
ostringstream out;
- out << resolutionList[i].w << "x" << resolutionList[i].h;
+ out << resolutionList[i].x << "x" << resolutionList[i].y;
resolutions->addItem(out.str());
//Check if current resolution matches, select it.
- if(resolutionList[i].w==currentRes.w && resolutionList[i].h==currentRes.h){
+ if(resolutionList[i].x==currentRes.x && resolutionList[i].y==currentRes.y){
resolutions->value=i;
}
}
//Add current resolution if it isn't already in the list.
if(resolutions->value==-1){
ostringstream out;
- out << currentRes.w << "x" << currentRes.h;
+ out << currentRes.x << "x" << currentRes.y;
resolutions->addItem(out.str());
resolutions->value=resolutions->item.size()-1;
}
lastRes=resolutions->value;
tabGeneral->addChild(resolutions);
obj=new GUILabel(imageManager,renderer,column1X,3*lineHeight,columnW,36,_("Language"));
tabGeneral->addChild(obj);
//Create GUI list with available languages.
langs = new GUISingleLineListBox(imageManager,renderer,column2X,3*lineHeight,columnW,36);
langs->name="lstLanguages";
/// TRANSLATORS: as detect user's language automatically
langs->addItem("",_("Auto-Detect"));
langs->addItem("en","English");
//Get a list of every available language.
set<tinygettext::Language> languages = dictionaryManager->get_languages();
for (set<tinygettext::Language>::iterator s0 = languages.begin(); s0 != languages.end(); ++s0){
//If language in loop is the same in config file, then select it
if(getSettings()->getValue("lang")==s0->str()){
lastLang=distance(languages.begin(),s0)+2;
}
//Add language in loop to list and listbox.
langs->addItem(s0->str(),s0->get_name());
}
//If Auto or English are selected.
if(getSettings()->getValue("lang")==""){
lastLang=0;
}else if(getSettings()->getValue("lang")=="en"){
lastLang=1;
}
langs->value=lastLang;
tabGeneral->addChild(langs);
obj=new GUILabel(imageManager,renderer,column1X,4*lineHeight,columnW,36,_("Theme"));
obj->name="theme";
tabGeneral->addChild(obj);
//Create the theme option gui element.
theme=new GUISingleLineListBox(imageManager,renderer,column2X,4*lineHeight,columnW,36);
theme->name="lstTheme";
//Vector containing the theme locations and names.
vector<pair<string,string> > themes;
vector<string> v=enumAllDirs(getUserPath(USER_DATA)+"themes/");
for(vector<string>::iterator i=v.begin(); i!=v.end(); ++i){
string location=getUserPath(USER_DATA)+"themes/"+*i;
themes.push_back(pair<string,string>(location,*i));
}
vector<string> v2=enumAllDirs(getDataPath()+"themes/");
for(vector<string>::iterator i=v2.begin(); i!=v2.end(); ++i){
string location=getDataPath()+"themes/"+*i;
themes.push_back(pair<string,string>(location,*i));
}
//Try to find the configured theme so we can display it.
int value=-1;
for(vector<pair<string,string> >::iterator i=themes.begin(); i!=themes.end(); ++i){
if(i->first==themeName) {
value=i-themes.begin();
}
}
theme->addItems(themes);
if(value==-1)
value=theme->item.size()-1;
theme->value=value;
//NOTE: We call the event handling method to correctly set the themename.
GUIEventCallback_OnEvent(imageManager,renderer,"lstTheme",theme,GUIEventChange);
theme->eventCallback=this;
tabGeneral->addChild(theme);
//Proxy settings.
obj=new GUILabel(imageManager,renderer,column1X,5*lineHeight,columnW,36,_("Internet proxy"));
obj->name="chkProxy";
obj->eventCallback=this;
tabGeneral->addChild(obj);
obj=new GUITextBox(imageManager,renderer,column2X,5*lineHeight,columnW,36,internetProxy.c_str());
obj->name="txtProxy";
obj->eventCallback=this;
tabGeneral->addChild(obj);
obj=new GUICheckBox(imageManager,renderer,column1X,6*lineHeight,columnW,36,_("Fullscreen"),fullscreen?1:0);
obj->name="chkFullscreen";
obj->eventCallback=this;
tabGeneral->addChild(obj);
obj = new GUICheckBox(imageManager, renderer, column1X, 7 * lineHeight, columnW, 36, _("Quick record"), quickrec ? 1 : 0);
obj->name = "chkQuickRec";
obj->eventCallback = this;
tabGeneral->addChild(obj);
obj=new GUICheckBox(imageManager,renderer,column2X,6*lineHeight,columnW,36,_("Internet"),internet?1:0);
obj->name="chkInternet";
obj->eventCallback=this;
tabGeneral->addChild(obj);
obj=new GUICheckBox(imageManager,renderer,column2X,7*lineHeight,columnW,36,_("Fade transition"),fade?1:0);
obj->name="chkFade";
obj->eventCallback=this;
tabGeneral->addChild(obj);
//Create the controls tab.
tabControls=inputMgr.showConfig(imageManager,renderer,SCREEN_HEIGHT-210);
tabControls->top=140;
tabControls->visible=false;
GUIObjectRoot->addChild(tabControls);
//Save original keys.
for(int i=0;i<INPUTMGR_MAX;i++){
tmpKeys[i]=inputMgr.getKeyCode((InputManagerKeys)i,false);
tmpAlternativeKeys[i]=inputMgr.getKeyCode((InputManagerKeys)i,true);
}
//Create buttons.
cmdBack = new GUIButton(imageManager, renderer, SCREEN_WIDTH*0.3, SCREEN_HEIGHT - 60, -1, 36, _("Cancel"), 0, true, true, GUIGravityCenter);
cmdBack->name = "cmdBack";
cmdBack->eventCallback = this;
GUIObjectRoot->addChild(cmdBack);
cmdSave = new GUIButton(imageManager, renderer, SCREEN_WIDTH*0.7, SCREEN_HEIGHT - 60, -1, 36, _("Save Changes"), 0, true, true, GUIGravityCenter);
cmdSave->name = "cmdSave";
cmdSave->eventCallback = this;
GUIObjectRoot->addChild(cmdSave);
}
static string convertInt(int i){
stringstream ss;
ss << i;
return ss.str();
}
void Options::GUIEventCallback_OnEvent(ImageManager& imageManager, SDL_Renderer& renderer, std::string name,GUIObject* obj,int eventType){
//Check what type of event it was.
if(eventType==GUIEventClick){
if(name=="cmdBack"){
//Reset the key changes.
for(int i=0;i<INPUTMGR_MAX;i++){
inputMgr.setKeyCode((InputManagerKeys)i,tmpKeys[i],false);
inputMgr.setKeyCode((InputManagerKeys)i,tmpAlternativeKeys[i],true);
}
//Reset the music volume.
getMusicManager()->setVolume(atoi(getSettings()->getValue("music").c_str()));
Mix_Volume(-1,atoi(getSettings()->getValue("sound").c_str()));
//And goto the main menu.
setNextState(STATE_MENU);
}else if(name=="cmdSave"){
//Save is pressed thus save
char s[64];
sprintf(s,"%d",soundSlider->value);
getSettings()->setValue("sound",s);
sprintf(s,"%d",musicSlider->value);
getSettings()->setValue("music",s);
getMusicManager()->setEnabled(musicSlider->value>0);
Mix_Volume(-1,soundSlider->value);
getSettings()->setValue("fullscreen",fullscreen?"1":"0");
getSettings()->setValue("internet",internet?"1":"0");
getSettings()->setValue("theme",themeName);
getSettings()->setValue("fading",fade?"1":"0");
getSettings()->setValue("quickrecord",quickrec?"1":"0");
//Before loading the theme remove the previous one from the stack.
objThemes.removeTheme();
loadTheme(imageManager,renderer,themeName);
if(!useProxy)
internetProxy.clear();
getSettings()->setValue("internet-proxy",internetProxy);
getSettings()->setValue("lang",langs->getName());
//Is resolution from the list or is it user defined in config file
if(resolutions->value<(int)resolutionList.size()){
- getSettings()->setValue("width",convertInt(resolutionList[resolutions->value].w));
- getSettings()->setValue("height",convertInt(resolutionList[resolutions->value].h));
+ getSettings()->setValue("width",convertInt(resolutionList[resolutions->value].x));
+ getSettings()->setValue("height",convertInt(resolutionList[resolutions->value].y));
}else{
- getSettings()->setValue("width",convertInt(currentRes.w));
- getSettings()->setValue("height",convertInt(currentRes.h));
+ getSettings()->setValue("width",convertInt(currentRes.x));
+ getSettings()->setValue("height",convertInt(currentRes.y));
}
//Save the key configuration.
inputMgr.saveConfig();
//Save the settings.
saveSettings();
//Before we return check if some .
if(restartFlag || resolutions->value!=lastRes){
//The resolution changed so we need to recreate the screen.
if(!createScreen()){
//Screen creation failed so set to safe settings.
getSettings()->setValue("fullscreen","0");
- getSettings()->setValue("width",convertInt(resolutionList[lastRes].w));
- getSettings()->setValue("height",convertInt(resolutionList[lastRes].h));
+ getSettings()->setValue("width",convertInt(resolutionList[lastRes].x));
+ getSettings()->setValue("height",convertInt(resolutionList[lastRes].y));
if(!createScreen()){
//Everything fails so quit.
setNextState(STATE_EXIT);
return;
}
}
//The screen is created, now load the (menu) theme.
if(!loadTheme(imageManager,renderer,"")){
//Loading the theme failed so quit.
setNextState(STATE_EXIT);
return;
}
}
if(langs->value!=lastLang){
//We set the language.
language=langs->getName();
if (language.empty()) {
// The language is set to auto-detect.
FL_Locale *locale;
FL_FindLocale(&locale, FL_MESSAGES);
language = locale->lang;
if (locale->country != NULL){
language += string("_") + string(locale->country);
}
if (locale->variant != NULL){
language += string("@") + string(locale->variant);
}
FL_FreeLocale(&locale);
}
dictionaryManager->set_language(tinygettext::Language::from_name(language));
+
+#ifdef WIN32
+ //Some ad-hoc fix for Windows since it accepts "zh-CN" but not "zh_CN"
+ std::string language2;
+ for (auto c : language) {
+ if (isalnum(c)) language2.push_back(c);
+ else if (c == '_') language2.push_back('-');
+ else break;
+ }
+ const char* languagePtr = language2.c_str();
+#else
+ const char* languagePtr = language.c_str();
+#endif
+
+ tfm::setLocale(languagePtr);
+ setlocale(LC_TIME, languagePtr);
+
getLevelPackManager()->updateLanguage();
-
+
//And reload the font.
if(!loadFonts()){
//Loading failed so quit.
setNextState(STATE_EXIT);
return;
}
}
//Now return to the main menu.
setNextState(STATE_MENU);
}else if(name=="chkFullscreen"){
fullscreen=obj->value?true:false;
//Check if fullscreen changed.
if(fullscreen==getSettings()->getBoolValue("fullscreen")){
//We disable the restart message flag.
restartFlag=false;
}else{
//We set the restart message flag.
restartFlag=true;
}
}else if(name=="chkInternet"){
internet=obj->value?true:false;
}else if(name=="chkProxy"){
useProxy=obj->value?true:false;
}else if(name=="chkFade"){
fade=obj->value?true:false;
}else if(name=="chkQuickRec"){
quickrec=obj->value?true:false;
}
}
if(name=="lstTheme"){
if(theme!=NULL && theme->value>=0 && theme->value<(int)theme->item.size()){
//Convert the themeName to contain %DATA%, etc...
themeName=compressFileName(theme->item[theme->value].first);
}
}else if(name=="txtProxy"){
internetProxy=obj->caption;
//Check if the internetProxy field is empty.
useProxy=!internetProxy.empty();
}else if(name=="sldMusic"){
getMusicManager()->setEnabled(musicSlider->value>0);
getMusicManager()->setVolume(musicSlider->value);
}else if(name=="sldSound"){
Mix_Volume(-1,soundSlider->value);
if(lastJumpSound==0){
getSoundManager()->playSound("jump");
lastJumpSound=15;
}
}
if(name=="lstTabs"){
if(obj->value==0){
tabGeneral->visible=true;
tabControls->visible=false;
}else{
tabGeneral->visible=false;
tabControls->visible=true;
}
}
}
void Options::handleEvents(ImageManager& imageManager, SDL_Renderer& renderer){
//Check keyboard navigation.
if (tabGeneral && tabGeneral->visible) {
if (inputMgr.isKeyDownEvent(INPUTMGR_TAB)) {
isKeyboardOnly = true;
section = (section == 2) ? 3 : 2;
//Update selection.
if (section == 2) {
tabGeneral->selectNextControl(1, -1);
} else {
tabGeneral->setSelectedControl(-1);
}
}
if (section == 2) {
tabGeneral->handleKeyboardNavigationEvents(imageManager, renderer, UpDownFocus | ReturnControls | LeftRightControls);
} else if (section == 3) {
if (inputMgr.isKeyDownEvent(INPUTMGR_DOWN) || inputMgr.isKeyDownEvent(INPUTMGR_RIGHT)) {
isKeyboardOnly = true;
section2++;
if (section2 > 3) section2 = 1;
} else if (inputMgr.isKeyDownEvent(INPUTMGR_UP) || inputMgr.isKeyDownEvent(INPUTMGR_LEFT)) {
isKeyboardOnly = true;
section2--;
if (section2 < 1) section2 = 3;
}
if (isKeyboardOnly && inputMgr.isKeyDownEvent(INPUTMGR_SELECT) && section == 3) {
if (section2 == 1) {
GUIEventCallback_OnEvent(imageManager, renderer, cmdBack->name, cmdBack, GUIEventClick);
} else if (section2 == 2) {
GUIEventCallback_OnEvent(imageManager, renderer, cmdSave->name, cmdSave, GUIEventClick);
}
}
}
}
//Process mouse event only when it's not keyboard only mode.
if (!isKeyboardOnly) {
//Get the x and y location of the mouse.
int x, y;
SDL_GetMouseState(&x, &y);
//Check icon.
if (event.type == SDL_MOUSEMOTION || event.type == SDL_MOUSEBUTTONDOWN){
if (y >= SCREEN_HEIGHT - 56 && y < SCREEN_HEIGHT - 8 && x >= SCREEN_WIDTH - 56)
clearIconHower = true;
else
clearIconHower = false;
}
}
//Update highlight on keyboard only mode.
if (isKeyboardOnly) {
cmdBack->state = (section == 3 && section2 == 1) ? 1 : 0;
cmdSave->state = (section == 3 && section2 == 2) ? 1 : 0;
clearIconHower = (section == 3 && section2 == 3);
}
if ((isKeyboardOnly ? inputMgr.isKeyDownEvent(INPUTMGR_SELECT) :
(event.type == SDL_MOUSEBUTTONUP && event.button.button == SDL_BUTTON_LEFT)) && clearIconHower)
{
if(msgBox(imageManager,renderer,_("Do you really want to reset level progress?"),MsgBoxYesNo,_("Warning"))==MsgBoxYes){
//We delete the progress folder.
#ifdef WIN32
removeDirectory((getUserPath()+"progress").c_str());
createDirectory((getUserPath()+"progress").c_str());
#else
removeDirectory((getUserPath(USER_DATA)+"/progress").c_str());
createDirectory((getUserPath(USER_DATA)+"/progress").c_str());
#endif
//Reset statistics.
statsMgr.reloadCompletedLevelsAndAchievements();
}
}
//Check if we need to quit, if so enter the exit state.
if(event.type==SDL_QUIT){
setNextState(STATE_EXIT);
}
//Check if the escape button is pressed, if so go back to the main menu.
if (inputMgr.isKeyDownEvent(INPUTMGR_ESCAPE) && (tabControls == NULL || !tabControls->visible)) {
setNextState(STATE_MENU);
}
}
void Options::logic(ImageManager&, SDL_Renderer&){
//Increase the lastJumpSound variable if needed.
if(lastJumpSound!=0){
lastJumpSound--;
}
}
void Options::render(ImageManager&, SDL_Renderer& renderer){
//Draw background.
objThemes.getBackground(true)->draw(renderer);
objThemes.getBackground(true)->updateAnimation();
//Now render the title.
drawTitleTexture(SCREEN_WIDTH, *title, renderer);
//Check if an icon is selected/highlighted and draw tooltip
if(clearIconHower){
const SDL_Rect texSize = rectFromTexture(*clearTooltip);
drawGUIBox(-2,SCREEN_HEIGHT-texSize.h-2,texSize.w+4,texSize.h+4,renderer,0xFFFFFFFF);
applyTexture(0,SCREEN_HEIGHT-texSize.h,clearTooltip,renderer);
}
//Draw border of icon if it's keyboard only mode.
if (isKeyboardOnly && clearIconHower) {
drawGUIBox(SCREEN_WIDTH - 52, SCREEN_HEIGHT - 52, 40, 40, renderer, 0xFFFFFF40);
}
//Draw icon.
applyTexture(SCREEN_WIDTH-48,SCREEN_HEIGHT-48,*clearIcon,renderer);
//NOTE: The rendering of the GUI is done in Main.
}
void Options::resize(ImageManager& imageManager, SDL_Renderer& renderer){
//Recreate the gui to fit the new resolution.
createGUI(imageManager,renderer);
}
diff --git a/src/Settings.cpp b/src/Settings.cpp
index c6e0def..bdc988d 100644
--- a/src/Settings.cpp
+++ b/src/Settings.cpp
@@ -1,276 +1,265 @@
/*
* Copyright (C) 2011-2012 Me and My Shadow
*
* This file is part of Me and My Shadow.
*
* Me and My Shadow is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Me and My Shadow is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Me and My Shadow. If not, see <http://www.gnu.org/licenses/>.
*/
#include "Settings.h"
#include "FileManager.h"
#include <SDL.h>
#include <string>
#include <stdio.h>
using namespace std;
// Hardcode the addon url unless specified by command line arguments
#define DEFAULT_ADDON_URL "https://github.com/acmepjz/meandmyshadow-addons/raw/master/addons05"
Settings::Settings(const string& fileName): fileName(fileName){
char s[32];
settings["sound"]="128";
settings["music"]="128";
settings["musiclist"]="default";
settings["fullscreen"]="0";
settings["width"]="800";
settings["height"]="600";
settings["resizable"]="1";
settings["theme"]="%DATA%/themes/Cloudscape";
settings["internet"]="1";
settings["lastlevelpack"]="tutorial";
settings["internet-proxy"]="";
settings["lang"]="";
settings["addon_url"] = DEFAULT_ADDON_URL;
//The record mode.
settings["quickrecord"]="0";
//Boolean if fading between states is enabled.
settings["fading"]="1";
//Key settings.
sprintf(s,"%d",(int)SDLK_UP);
settings["key_up"]=settings["key_jump"]=s;
sprintf(s,"%d",(int)SDLK_DOWN);
settings["key_down"]=settings["key_action"]=s; //SDLK_DOWN
sprintf(s,"%d",(int)SDLK_LEFT);
settings["key_left"]=s; //SDLK_LEFT
sprintf(s,"%d",(int)SDLK_RIGHT);
settings["key_right"]=s; //SDLK_RIGHT
sprintf(s,"%d",(int)SDLK_SPACE);
settings["key_space"]=s; //SDLK_SPACE
settings["key_cancelRecording"]="0"; //not set by default
sprintf(s,"%d",(int)SDLK_ESCAPE);
settings["key_escape"]=s; //SDLK_ESCAPE
sprintf(s,"%d",(int)SDLK_r);
settings["key_restart"]=s; //SDLK_r
sprintf(s,"%d",(int)SDLK_TAB);
settings["key_tab"]=s;
sprintf(s,"%d",(int)SDLK_F2);
settings["key_save"]=s; //SDLK_F2
sprintf(s,"%d",(int)SDLK_F3);
settings["key_load"]=s; //SDLK_F3
sprintf(s,"%d",(int)SDLK_F4);
settings["key_swap"]=s; //SDLK_F4
sprintf(s,"%d",(int)SDLK_F5);
settings["key_teleport"]=s; //SDLK_F5
sprintf(s,"%d",(int)SDLK_F12);
settings["key_suicide"]=s;
sprintf(s,"%d",(int)SDLK_RSHIFT);
settings["key_shift"]=s; //SDLK_RSHIFT
sprintf(s,"%d",(int)SDLK_PAGEUP);
settings["key_next"]=s; //SDLK_PAGEUP
sprintf(s,"%d",(int)SDLK_PAGEDOWN);
settings["key_previous"]=s; //SDLK_PAGEDOWN
sprintf(s,"%d",(int)SDLK_RETURN);
settings["key_select"]=s; //SDLK_RETURN
settings["key_up2"]=settings["key_down2"]=settings["key_left2"]=settings["key_right2"]=
settings["key_jump2"]=settings["key_action2"]=
settings["key_space2"]=settings["key_cancelRecording2"]=
settings["key_escape2"]=settings["key_restart2"]=settings["key_tab2"]=
settings["key_save2"]=settings["key_load2"]=settings["key_swap2"]=settings["key_teleport2"]=
settings["key_suicide2"]=settings["key_shift2"]=settings["key_next2"]=settings["key_previous2"]=
settings["key_select2"]="0";
}
void Settings::parseFile(int version){
//Open the config file for reading.
ifstream file;
file.open(fileName.c_str());
if(!file){
//Check if the file exists.
if(fileExists(fileName.c_str())){
cerr<<"ERROR: Unable to read config file, default values will be used!"<<endl;
}else{
cout<<"Creating a new config file."<<endl;
createFile();
}
//No need to parse the unreadable or newly created config file.
return;
}
//Read the config file line by line.
string line;
while(getline(file,line)){
string temp = line;
unComment(temp);
if(temp.empty() || empty(temp))
continue;
//The line is good so we parse it.
parseLine(temp,version);
}
//And close the file.
file.close();
// Hardcode the addon url unless specified by command line arguments
settings["addon_url"] = DEFAULT_ADDON_URL;
}
void Settings::parseLine(const string &line,int version){
if((line.find('=') == line.npos) || !validLine(line))
cerr<<"WARNING: illegal line in config file!"<<endl;
string temp = line;
temp.erase(0, temp.find_first_not_of("\t "));
int seperator = temp.find('=');
//Get the key and trim it.
string key, value;
key = line.substr(0, seperator);
if(key.find('\t')!=line.npos || key.find(' ')!=line.npos)
key.erase(key.find_first_of("\t "));
//Get the value and trim it.
value = line.substr(seperator + 1);
value.erase(0, value.find_first_not_of("\t "));
value.erase(value.find_last_not_of("\t ") + 1);
//Check the version.
//Currently in V0.5 we don't read key config saved in versions which is earlier than V0.5, since they are incompatible (SDL1.2 vs SDL2).
if (version < 0x000500) {
if (key.size() >= 4 && key.substr(0, 4) == "key_") {
return;
}
}
//Add the setting to the settings map.
setValue(key,value);
}
bool Settings::validLine(const string &line){
string temp = line;
temp.erase(0, temp.find_first_not_of("\t "));
if(temp[0] == '=')
return false;
for(size_t i = temp.find('=') + 1; i < temp.length(); i++)
return true;
return false;
}
void Settings::unComment(string &line){
if(line.find('#') != line.npos)
line.erase(line.find('#'));
}
bool Settings::empty(const string &line){
return (line.find_first_not_of(' ')==line.npos);
}
string Settings::getValue(const string &key){
if(settings.find(key) == settings.end()){
cerr<<"WARNING: Key "<<key<<" couldn't be found!"<<endl;
return "";
}
return settings[key];
}
bool Settings::getBoolValue(const string &key){
if(settings.find(key) == settings.end()){
cerr<<"WARNING: Key "<<key<<" couldn't be found!"<<endl;
return false;
}
return (settings[key] != "0");
}
void Settings::setValue(const string &key, const string &value){
settings[key]=value;
}
void Settings::createFile(){
ofstream file;
file.open(fileName.c_str());
//Default Config file.
- file<<"#MeAndMyShadow config file. Created on "<<endl;
+ file<<"# MeAndMyShadow config file"<<endl;
map<string, string>::iterator iter;
for(iter = settings.begin(); iter != settings.end(); ++iter){
file << iter->first << " = " << iter->second << endl;
}
//And close the file.
file.close();
}
bool Settings::save(){
//Open the settings file.
ofstream file;
file.open(fileName.c_str());
//Check if the file could be opened.
if(!file)
return false;
-
- //First get the date and time.
- time_t rawtime;
- struct tm* timedate;
- char str_time[80];
-
- time(&rawtime);
- timedate=localtime(&rawtime);
-
- //NOTE: Function asctime() is marked obsolete in POSIX. So we're using strftime() instead.
- strftime(str_time,80,"%a %b %d %H:%M:%S %Y",timedate);
-
+
//Now write it to the first line of the config file.
- file<<"#MeAndMyShadow config file. Created on "<<str_time<<endl;
+ file<<"# MeAndMyShadow config file"<<endl;
//Loop through the settings and save them.
map<string,string>::const_iterator iter;
for(iter=settings.begin(); iter!=settings.end(); ++iter){
if (iter->first == "addon_url") {
// Hardcode the addon url
file << iter->first << " = " DEFAULT_ADDON_URL << endl;
} else {
file << iter->first << " = " << iter->second << endl;
}
}
file.close();
//No errors so return true.
return true;
}
diff --git a/src/StatisticsManager.cpp b/src/StatisticsManager.cpp
index f63fb2d..4dd015c 100644
--- a/src/StatisticsManager.cpp
+++ b/src/StatisticsManager.cpp
@@ -1,810 +1,804 @@
/*
* Copyright (C) 2012 Me and My Shadow
*
* This file is part of Me and My Shadow.
*
* Me and My Shadow is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Me and My Shadow is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Me and My Shadow. If not, see <http://www.gnu.org/licenses/>.
*/
#include "StatisticsManager.h"
#include "FileManager.h"
#include "TreeStorageNode.h"
#include "POASerializer.h"
#include "Functions.h"
#include "LevelPackManager.h"
#include "MusicManager.h"
#include "SoundManager.h"
#include "ThemeManager.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <iostream>
#include <fstream>
#include <sstream>
#include <vector>
#include <map>
#include "libs/tinyformat/tinyformat.h"
#include <SDL_ttf.h>
#if defined(WIN32)
#define PRINTF_LONGLONG "%I64d"
#else
#define PRINTF_LONGLONG "%lld"
#endif
using namespace std;
StatisticsManager statsMgr;
static const int achievementDisplayTime=(FPS*4500)/1000;
static const int achievementIntervalTime=achievementDisplayTime+(FPS*500)/1000;
static map<string,AchievementInfo*> avaliableAchievements;
//================================================================
StatisticsManager::StatisticsManager(){
bmDropShadow=NULL;
bmQuestionMark=NULL;
bmAchievement=NULL;
startTime=time(NULL);
tutorialLevels=0;
clear();
}
void StatisticsManager::clear(){
playerTravelingDistance=shadowTravelingDistance=0.0f;
playerJumps=shadowJumps
=playerDies=shadowDies
=playerSquashed=shadowSquashed
=completedLevels=silverLevels=goldLevels
=recordTimes=switchTimes=swapTimes=saveTimes=loadTimes
=playTime=levelEditTime
=createdLevels=tutorialCompleted=tutorialGold=0;
achievements.clear();
queuedAchievements.clear();
achievementTime=0;
currentAchievement=0;
if(bmAchievement){
bmAchievement.reset();
}
}
#define LOAD_STATS(var,func) { \
vector<string> &v=node.attributes[ #var ]; \
if(!v.empty() && !v[0].empty()) \
var=func(v[0].c_str()); \
}
void StatisticsManager::loadFile(const std::string& fileName){
clear();
ifstream file(fileName.c_str());
if(!file) return;
TreeStorageNode node;
POASerializer serializer;
if(!serializer.readNode(file,&node,true)) return;
//load statistics
LOAD_STATS(playerTravelingDistance,atof);
LOAD_STATS(shadowTravelingDistance,atof);
LOAD_STATS(playerJumps,atoi);
LOAD_STATS(shadowJumps,atoi);
LOAD_STATS(playerDies,atoi);
LOAD_STATS(shadowDies,atoi);
LOAD_STATS(playerSquashed,atoi);
LOAD_STATS(shadowSquashed,atoi);
LOAD_STATS(recordTimes,atoi);
LOAD_STATS(switchTimes,atoi);
LOAD_STATS(swapTimes,atoi);
LOAD_STATS(saveTimes,atoi);
LOAD_STATS(loadTimes,atoi);
LOAD_STATS(playTime,atoi);
LOAD_STATS(levelEditTime,atoi);
LOAD_STATS(createdLevels,atoi);
//load achievements.
//format is: name;time,name;time,...
{
vector<string> &v=node.attributes["achievements"];
for(unsigned int i=0;i<v.size();i++){
string s=v[i];
time_t t=0;
string::size_type lps=s.find(';');
if(lps!=string::npos){
string s1=s.substr(lps+1);
s=s.substr(0,lps);
long long n;
sscanf(s1.c_str(),PRINTF_LONGLONG,&n);
t=(time_t)n;
}
map<string,AchievementInfo*>::iterator it=avaliableAchievements.find(s);
if(it!=avaliableAchievements.end()){
OwnedAchievement ach={t,it->second};
achievements[it->first]=ach;
}
}
}
}
//Call when level edit is start
void StatisticsManager::startLevelEdit(){
levelEditStartTime=time(NULL);
}
//Call when level edit is end
void StatisticsManager::endLevelEdit(){
levelEditTime+=time(NULL)-levelEditStartTime;
}
//update in-game time
void StatisticsManager::updatePlayTime(){
time_t endTime=time(NULL);
playTime+=endTime-startTime;
startTime=endTime;
}
#define SAVE_STATS(var,pattern) { \
sprintf(s,pattern,var); \
node.attributes[ #var ].push_back(s); \
}
void StatisticsManager::saveFile(const std::string& fileName){
char s[64];
//update in-game time
updatePlayTime();
ofstream file(fileName.c_str());
if(!file) return;
TreeStorageNode node;
//save statistics
SAVE_STATS(playerTravelingDistance,"%.2f");
SAVE_STATS(shadowTravelingDistance,"%.2f");
SAVE_STATS(playerJumps,"%d");
SAVE_STATS(shadowJumps,"%d");
SAVE_STATS(playerDies,"%d");
SAVE_STATS(shadowDies,"%d");
SAVE_STATS(playerSquashed,"%d");
SAVE_STATS(shadowSquashed,"%d");
SAVE_STATS(recordTimes,"%d");
SAVE_STATS(switchTimes,"%d");
SAVE_STATS(swapTimes,"%d");
SAVE_STATS(saveTimes,"%d");
SAVE_STATS(loadTimes,"%d");
SAVE_STATS(playTime,"%d");
SAVE_STATS(levelEditTime,"%d");
SAVE_STATS(createdLevels,"%d");
//save achievements.
//format is: name;time,name;time,...
{
vector<string>& v=node.attributes["achievements"];
for(map<string,OwnedAchievement>::iterator it=achievements.begin();it!=achievements.end();++it){
stringstream strm;
char s[32];
long long n=it->second.achievedTime;
sprintf(s,PRINTF_LONGLONG,n);
strm<<it->first<<";"<<s;
v.push_back(strm.str());
}
}
POASerializer serializer;
serializer.writeNode(&node,file,true,true);
}
void StatisticsManager::loadPicture(SDL_Renderer& renderer, ImageManager& imageManager){
//Load drop shadow picture
bmDropShadow=imageManager.loadTexture(getDataPath()+"gfx/dropshadow.png", renderer);
bmQuestionMark=imageManager.loadImage(getDataPath()+"gfx/menu/questionmark.png");
}
void StatisticsManager::registerAchievements(ImageManager& imageManager){
if(!avaliableAchievements.empty()) return;
for(int i=0;achievementList[i].id!=NULL;i++){
avaliableAchievements[achievementList[i].id]=&achievementList[i];
if(achievementList[i].imageFile!=NULL){
achievementList[i].imageSurface = imageManager.loadImage(getDataPath()+achievementList[i].imageFile);
}
}
}
void StatisticsManager::render(ImageManager&,SDL_Renderer &renderer){
if(achievementTime==0 && !bmAchievement && currentAchievement<(int)queuedAchievements.size()){
//create surface
bmAchievement=createAchievementSurface(renderer, queuedAchievements[currentAchievement++]);
//FIXME: Draw the box.
//drawGUIBox(0,0,bmAchievement->w,bmAchievement->h,bmAchievement,0xFFFFFF00);
//check if queue is empty
if(currentAchievement>=(int)queuedAchievements.size()){
queuedAchievements.clear();
currentAchievement=0;
}
//play a sound
getSoundManager()->playSound("achievement", 1, false, 32);
}
//check if we need to display achievements
if(bmAchievement){
achievementTime++;
if(achievementTime<=0){
return;
}else if(achievementTime<=5){
drawAchievement(renderer,achievementTime);
}else if(achievementTime<=achievementDisplayTime-5){
drawAchievement(renderer,5);
}else if(achievementTime<achievementDisplayTime){
drawAchievement(renderer,achievementDisplayTime-achievementTime);
}else if(achievementTime>=achievementIntervalTime){
if(bmAchievement){
bmAchievement.reset();
}
achievementTime=0;
}
}
}
void StatisticsManager::newAchievement(const std::string& id,bool save){
//check avaliable achievements
map<string,AchievementInfo*>::iterator it=avaliableAchievements.find(id);
if(it==avaliableAchievements.end()) return;
//check if already have this achievement
if(save){
map<string,OwnedAchievement>::iterator it2=achievements.find(id);
if(it2!=achievements.end()) return;
OwnedAchievement ach={time(NULL),it->second};
achievements[id]=ach;
}
//add it to queue
queuedAchievements.push_back(it->second);
}
time_t StatisticsManager::achievedTime(const std::string& id) {
auto it = achievements.find(id);
if (it == achievements.end()) return 0;
else return it->second.achievedTime;
}
float StatisticsManager::getAchievementProgress(AchievementInfo* info){
if(!strcmp(info->id,"experienced")){
return float(completedLevels)/50.0f*100.0f;
}
if(!strcmp(info->id,"expert")){
return float(goldLevels)/50.0f*100.0f;
}
if(!strcmp(info->id,"tutorial")){
if(tutorialLevels>0)
return float(tutorialCompleted)/float(tutorialLevels)*100.0f;
else
return 0.0f;
}
if(!strcmp(info->id,"tutorialGold")){
if(tutorialLevels>0)
return float(tutorialGold)/float(tutorialLevels)*100.0f;
else
return 0.0f;
}
if(!strcmp(info->id,"create50")){
return float(createdLevels)/50.0f*100.0f;
}
if(!strcmp(info->id,"frog")){
return float(playerJumps+shadowJumps)/1000.0f*100.0f;
}
if(!strcmp(info->id,"die50")){
return float(playerDies+shadowDies)/50.0f*100.0f;
}
if(!strcmp(info->id,"die1000")){
return float(playerDies+shadowDies)/1000.0f*100.0f;
}
if(!strcmp(info->id,"suqash50")){
return float(playerSquashed+shadowSquashed)/50.0f*100.0f;
}
if(!strcmp(info->id,"travel100")){
return (playerTravelingDistance+shadowTravelingDistance)/100.0f*100.0f;
}
if(!strcmp(info->id,"travel1k")){
return (playerTravelingDistance+shadowTravelingDistance)/1000.0f*100.0f;
}
if(!strcmp(info->id,"travel10k")){
return (playerTravelingDistance+shadowTravelingDistance)/10000.0f*100.0f;
}
if(!strcmp(info->id,"travel42k")){
return (playerTravelingDistance+shadowTravelingDistance)/42195.0f*100.0f;
}
if(!strcmp(info->id,"record100")){
return float(recordTimes)/100.0f*100.0f;
}
if(!strcmp(info->id,"record1k")){
return float(recordTimes)/1000.0f*100.0f;
}
if(!strcmp(info->id,"switch100")){
return float(switchTimes)/100.0f*100.0f;
}
if(!strcmp(info->id,"switch1k")){
return float(switchTimes)/1000.0f*100.0f;
}
if(!strcmp(info->id,"swap100")){
return float(swapTimes)/100.0f*100.0f;
}
if(!strcmp(info->id,"swap1k")){
return float(swapTimes)/1000.0f*100.0f;
}
//not found
return 0.0f;
}
SharedTexture StatisticsManager::createAchievementSurface(SDL_Renderer& renderer, AchievementInfo* info,SDL_Rect* rect,bool showTip,const time_t *achievedTime){
if(info==NULL || info->id==NULL) return NULL;
//prepare text
SurfacePtr title0(nullptr);
SurfacePtr title1(nullptr);
vector<SDL_Surface*> descSurfaces;
SDL_Color fg = objThemes.getTextColor(true);
int fontHeight=TTF_FontLineSkip(fontText);
bool showDescription=false;
bool showImage=false;
float achievementProgress=0.0f;
if(showTip){
title0.reset(TTF_RenderUTF8_Blended(fontText,_("New achievement:"),fg));
title1.reset(TTF_RenderUTF8_Blended(fontGUISmall,_(info->name),fg));
showDescription=showImage=true;
}else if(achievedTime){
- char s[128];
+ char s[256];
strftime(s,sizeof(s),"%c",localtime(achievedTime));
- stringstream strm;
- tinyformat::format(strm,_("Achieved on %s"),s);
-
title0.reset(TTF_RenderUTF8_Blended(fontGUISmall,_(info->name),fg));
- title1.reset(TTF_RenderUTF8_Blended(fontText,strm.str().c_str(),fg));
+ title1.reset(TTF_RenderUTF8_Blended(fontText, tfm::format(_("Achieved on %s"), (char*)s).c_str(), fg));
showDescription=showImage=true;
}else if(info->displayStyle==ACHIEVEMENT_HIDDEN){
title0.reset(TTF_RenderUTF8_Blended(fontGUISmall,_("Unknown achievement"),fg));
}else{
if(info->displayStyle==ACHIEVEMENT_PROGRESS){
achievementProgress=getAchievementProgress(info);
- stringstream strm;
- tinyformat::format(strm,_("Achieved %1.0f%%"),achievementProgress);
-
- title1.reset(TTF_RenderUTF8_Blended(fontText,strm.str().c_str(),fg));
+ title1.reset(TTF_RenderUTF8_Blended(fontText, tfm::format(_("Achieved %1.0f%%"), achievementProgress).c_str(), fg));
}else{
title1.reset(TTF_RenderUTF8_Blended(fontText,_("Not achieved"),fg));
}
title0.reset(TTF_RenderUTF8_Blended(fontGUISmall,_(info->name),fg));
showDescription= info->displayStyle==ACHIEVEMENT_ALL || info->displayStyle==ACHIEVEMENT_PROGRESS;
showImage=true;
}
if(info->description!=NULL && showDescription){
string description=_(info->description);
string::size_type lps=0,lpe;
for(;;){
lpe=description.find('\n',lps);
if(lpe==string::npos){
descSurfaces.push_back(TTF_RenderUTF8_Blended(fontText,(description.substr(lps)+' ').c_str(),fg));
break;
}else{
descSurfaces.push_back(TTF_RenderUTF8_Blended(fontText,(description.substr(lps,lpe-lps)+' ').c_str(),fg));
lps=lpe+1;
}
}
}
//calculate the size
int w=0,h=0,w1=8,h1=0;
if(title0!=NULL){
if(title0->w>w) w=title0->w;
h1+=title0->h;
}
if(title1!=NULL){
if(title1->w>w) w=title1->w;
h1+=title1->h;
/*//calc progress bar size
if(!showTip && !achievedTime && info->displayStyle==ACHIEVEMENT_PROGRESS){
h1+=4;
}*/
}
const int preferredImageWidth = 50;
const int preferredImageHeight = 50;
if(showImage){
if(info->imageSurface!=NULL){
// NEW: we have the preferred image size
const int width = std::max(info->r.w, preferredImageWidth);
const int height = std::max(info->r.h, preferredImageHeight);
w1+=width+8;
w+=width+8;
if(height>h1) h1=height;
}
}else{
w1+=bmQuestionMark->w+8;
w+=bmQuestionMark->w+8;
if(bmQuestionMark->h>h1) h1=bmQuestionMark->h;
}
h=h1+8;
for(unsigned int i=0;i<descSurfaces.size();i++){
if(descSurfaces[i]!=NULL){
if(descSurfaces[i]->w>w) w=descSurfaces[i]->w;
}
}
h+=descSurfaces.size()*fontHeight;
w+=16;
h+=16;
//check if size is specified
int left=0,top=0;
if(rect!=NULL){
//NOTE: SDL2 port. This was never used.
/* if(surface!=NULL){
left=rect->x;
top=rect->y;
}*/
if(rect->w>0) w=rect->w;
else rect->w=w;
rect->h=h;
}
//create surface if necessary
SurfacePtr surface = createSurface(w, h);
std::unique_ptr<SDL_Renderer,decltype(&SDL_DestroyRenderer)> surfaceRenderer(
SDL_CreateSoftwareRenderer(surface.get()), &SDL_DestroyRenderer);
//draw background
const SDL_Rect r={left,top,w,h};
if(showTip || achievedTime){
SDL_FillRect(surface.get(),&r,SDL_MapRGB(surface->format,255,255,255));
}else{
SDL_FillRect(surface.get(),&r,SDL_MapRGB(surface->format,192,192,192));
}
//draw horizontal separator
//FIXME: this is moved from StatisticsScreen::createGUI
if (!showTip) {
const SDL_Rect r0 = { left, top, w, 1 };
const SDL_Rect r1 = { left, top + h - 2, w, 1 };
const SDL_Rect r2 = { left, top + h - 1, w, 1 };
Uint32 c0 = achievedTime ? SDL_MapRGB(surface->format, 224, 224, 224) : SDL_MapRGB(surface->format, 168, 168, 168);
Uint32 c2 = achievedTime ? SDL_MapRGB(surface->format, 128, 128, 128) : SDL_MapRGB(surface->format, 96, 96, 96);
SDL_FillRect(surface.get(), &r0, c0);
SDL_FillRect(surface.get(), &r1, c0);
SDL_FillRect(surface.get(), &r2, c2);
}
//draw picture
if(showImage){
if(info->imageSurface){
// NEW: we have the preferred image size
SDL_Rect r={left+8,top+8+(h1-info->r.h)/2,0,0};
if (info->r.w < preferredImageWidth) r.x += (preferredImageWidth - info->r.w) / 2;
SDL_BlitSurface(info->imageSurface,&info->r,surface.get(),&r);
}
}else{
SDL_Rect r={left+8,top+8+(h1-bmQuestionMark->h)/2,0,0};
SDL_BlitSurface(bmQuestionMark,NULL,surface.get(),&r);
}
//draw text
h=8;
if(title0){
SDL_Rect r={left+w1,top+h,0,0};
SDL_BlitSurface(title0.get(),NULL,surface.get(),&r);
h+=title0->h;
}
if(title1){
SDL_Rect r={left+w1,top+h,0,0};
//Draw progress bar.
if(!showTip && !achievedTime && info->displayStyle==ACHIEVEMENT_PROGRESS){
//Draw borders.
SDL_Rect r1={r.x,r.y,w-8-r.x,title1->h};
drawGUIBox(r1.x,r1.y,r1.w,r1.h,*surfaceRenderer,0x1D);
//Draw progress.
r1.x++;
r1.y++;
r1.w=int(achievementProgress/100.0f*float(r1.w-2)+0.5f);
r1.h-=2;
SDL_SetRenderDrawColor(surfaceRenderer.get(),0,0,0,100);
SDL_RenderFillRect(surfaceRenderer.get(),&r1);
//shift the text a little bit (???)
r.x+=2;
r.y+=2;
}
//Draw text.
SDL_BlitSurface(title1.get(),NULL,surface.get(),&r);
}
h=h1+16;
for(unsigned int i=0;i<descSurfaces.size();i++){
if(descSurfaces[i]!=NULL){
SDL_Rect r={left+8,top+h+static_cast<int>(i)*fontHeight,0,0};
SDL_BlitSurface(descSurfaces[i],NULL,surface.get(),&r);
}
}
//clean up
for(unsigned int i=0;i<descSurfaces.size();i++){
if(descSurfaces[i]!=NULL){
SDL_FreeSurface(descSurfaces[i]);
}
}
//FIXME: Should we clear the vector here?
//over
return textureFromSurface(renderer, std::move(surface));
}
void StatisticsManager::drawAchievement(SDL_Renderer& renderer,int alpha){
if(!bmAchievement || alpha<=0) {
return;
}
if(alpha>5) alpha=5;
SDL_Rect r = rectFromTexture(*bmAchievement);
int w=0,h=0;
SDL_GetRendererOutputSize(&renderer, &w, &h);
r.x = w-32-r.w;
r.y = 32;
SDL_SetTextureAlphaMod(bmAchievement.get(), alpha*40);
applyTexture(r.x, r.y,bmAchievement, renderer);
if(!bmDropShadow) {
return;
}
//draw drop shadow - corner
{
int w1=r.w/2,w2=r.w-w1,h1=r.h/2,h2=r.h-h1;
if(w1>16) w1=16;
if(w2>16) w2=16;
if(h1>16) h1=16;
if(h2>16) h2=16;
const int x=(5-alpha)*64;
//top-left
SDL_Rect r1={x,0,w1+16,h1+16};//),r2={r.x-16,r.y-16,0,0};
SDL_Rect r2 ={r.x-16, r.y-16, r1.w, r1.h};
SDL_RenderCopy(&renderer, bmDropShadow.get(), &r1, &r2);
//top-right
r1.x=x+48-w2;r2.w=r1.w =w2+16;r2.x=r.x+r.w-w2;
SDL_RenderCopy(&renderer, bmDropShadow.get(), &r1, &r2);
//bottom-right
r1.y=48-h2;r2.h=r1.h=h2+16;r2.y=r.y+r.h-h2;
SDL_RenderCopy(&renderer, bmDropShadow.get(), &r1, &r2);
//bottom-left
r1.x=x;r2.w=r1.w=w1+16;r2.x=r.x-16;
SDL_RenderCopy(&renderer, bmDropShadow.get(), &r1, &r2);
}
//draw drop shadow - border
int i=r.w-32;
while(i>0){
const int ii=i>128?128:i;
//top
SDL_Rect r1={0,256-alpha*16,ii,16};
SDL_Rect r2={r.x+r.w-16-i,r.y-16,r1.w,r1.h};
SDL_RenderCopy(&renderer, bmDropShadow.get(), &r1, &r2);
//bottom
r1.x=128;r2.y=r.y+r.h;
SDL_RenderCopy(&renderer, bmDropShadow.get(), &r1, &r2);
i-=ii;
}
i=r.h-32;
while(i>0){
const int ii=i>128?128:i;
//top
SDL_Rect r1={512-alpha*16,0,16,ii};
SDL_Rect r2={r.x-16,r.y+r.h-16-i, r1.w, r1.h};
SDL_RenderCopy(&renderer, bmDropShadow.get(), &r1, &r2);
//bottom
r1.y=128;r2.x=r.x+r.w;
SDL_RenderCopy(&renderer, bmDropShadow.get(), &r1, &r2);
i-=ii;
}
}
void StatisticsManager::reloadCompletedLevelsAndAchievements(){
completedLevels=silverLevels=goldLevels=0;
LevelPackManager *lpm=getLevelPackManager();
vector<pair<string,string> > v=lpm->enumLevelPacks();
bool tutorial=false,tutorialIsGold=false;
for(unsigned int i=0;i<v.size();i++){
string& s=v[i].first;
LevelPack *levels=lpm->getLevelPack(s);
levels->loadProgress();
bool b=false;
if(s==lpm->tutorialLevelPackPath){
tutorialLevels=levels->getLevelCount();
tutorialCompleted=tutorialGold=0;
b=tutorial=tutorialIsGold=true;
}
for(int n=0,m=levels->getLevelCount();n<m;n++){
LevelPack::Level *lv=levels->getLevel(n);
int medal=lv->won;
if(medal){
if(lv->targetTime<0 || lv->time<=lv->targetTime)
medal++;
if(lv->targetRecordings<0 || lv->recordings<=lv->targetRecordings)
medal++;
completedLevels++;
if(b) tutorialCompleted++;
if(medal==2) silverLevels++;
if(medal==3){
goldLevels++;
if(b) tutorialGold++;
}
if(medal!=3 && b) tutorialIsGold=false;
}else if(b){
tutorial=tutorialIsGold=false;
}
}
}
//upadte achievements
updateLevelAchievements();
updateTutorialAchievementsInternal((tutorial?1:0)|(tutorialIsGold?2:0));
}
void StatisticsManager::reloadOtherAchievements(){
int i;
if(playTime>=7200) newAchievement("addicted");
if(playTime>=86400) newAchievement("loyalFan");
if(levelEditTime>=7200) newAchievement("constructor");
if(levelEditTime>=86400) newAchievement("constructor2");
if(createdLevels>=1) newAchievement("create1");
if(createdLevels>=50) newAchievement("create50");
i=playerJumps+shadowJumps;
if(i>=1000) newAchievement("frog");
i=playerDies+shadowDies;
if(i>=1) newAchievement("die1");
if(i>=50) newAchievement("die50");
if(i>=1000) newAchievement("die1000");
i=playerSquashed+shadowSquashed;
if(i>=1) newAchievement("squash1");
if(i>=50) newAchievement("squash50");
float d=playerTravelingDistance+shadowTravelingDistance;
if(d>=100.0f) newAchievement("travel100");
if(d>=1000.0f) newAchievement("travel1k");
if(d>=10000.0f) newAchievement("travel10k");
if(d>=42195.0f) newAchievement("travel42k");
if(recordTimes>=100) newAchievement("record100");
if(recordTimes>=1000) newAchievement("record1k");
if(switchTimes>=100) newAchievement("switch100");
if(switchTimes>=1000) newAchievement("switch1k");
if(swapTimes>=100) newAchievement("swap100");
if(swapTimes>=1000) newAchievement("swap1k");
if(saveTimes>=1000) newAchievement("save1k");
if(loadTimes>=1000) newAchievement("load1k");
if (version.find("Development") != string::npos
|| version.find("Alpha") != string::npos
|| version.find("Beta") != string::npos
|| version.find("RC") != string::npos
|| version.find("Candidate") != string::npos)
{
newAchievement("programmer");
}
}
//Update level specified achievements.
//Make sure the completed level count is correct.
void StatisticsManager::updateLevelAchievements(){
if(completedLevels>=1) newAchievement("newbie");
if(goldLevels>=1) newAchievement("goodjob");
if(completedLevels>=50) newAchievement("experienced");
if(goldLevels>=50) newAchievement("expert");
}
//Update tutorial specified achievements.
//Make sure the level progress of tutorial is correct.
void StatisticsManager::updateTutorialAchievements(){
//find tutorial level pack
LevelPackManager *lpm=getLevelPackManager();
LevelPack *levels=lpm->getTutorialLevelPack();
if(levels==NULL) return;
bool tutorial=true,tutorialIsGold=true;
tutorialLevels=levels->getLevelCount();
tutorialCompleted=tutorialGold=0;
for(int n=0,m=levels->getLevelCount();n<m;n++){
LevelPack::Level *lv=levels->getLevel(n);
int medal=lv->won;
if(medal){
if(lv->targetTime<0 || lv->time<=lv->targetTime)
medal++;
if(lv->targetRecordings<0 || lv->recordings<=lv->targetRecordings)
medal++;
tutorialCompleted++;
if(medal!=3) tutorialIsGold=false;
else tutorialGold++;
}else{
tutorial=tutorialIsGold=false;
break;
}
}
//upadte achievements
updateTutorialAchievementsInternal((tutorial?1:0)|(tutorialIsGold?2:0));
}
//internal function
//flags: a bit-field value indicates which achievements we have.
void StatisticsManager::updateTutorialAchievementsInternal(int flags){
if(flags&1) newAchievement("tutorial");
if(flags&2) newAchievement("tutorialGold");
}
diff --git a/src/StatisticsScreen.cpp b/src/StatisticsScreen.cpp
index f4307de..7cd7997 100644
--- a/src/StatisticsScreen.cpp
+++ b/src/StatisticsScreen.cpp
@@ -1,411 +1,416 @@
/*
* Copyright (C) 2012 Me and My Shadow
*
* This file is part of Me and My Shadow.
*
* Me and My Shadow is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Me and My Shadow is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Me and My Shadow. If not, see <http://www.gnu.org/licenses/>.
*/
#include <stdio.h>
#include <string>
#include <vector>
#include <map>
#include "StatisticsManager.h"
#include "StatisticsScreen.h"
#include "Globals.h"
#include "Functions.h"
#include "ThemeManager.h"
#include "InputManager.h"
#include "GUIListBox.h"
#include "GUIScrollBar.h"
#include "EasterEggScreen.h"
#include <SDL_ttf.h>
-#include <array>
+
+#include "libs/tinyformat/tinyformat.h"
using namespace std;
//GUI events are handled here.
//name: The name of the element that invoked the event.
//obj: Pointer to the object that invoked the event.
//eventType: Integer containing the type of event.
void StatisticsScreen::GUIEventCallback_OnEvent(ImageManager& imageManager, SDL_Renderer& renderer, std::string name,GUIObject* obj,int eventType){
//Check what type of event it was.
if(eventType==GUIEventClick){
if(name=="cmdBack"){
//Goto the main menu.
setNextState(STATE_MENU);
}
}
}
//Constructor.
StatisticsScreen::StatisticsScreen(ImageManager& imageManager, SDL_Renderer& renderer){
//Update in-game time.
statsMgr.updatePlayTime();
//Render the title.
title = titleTextureFromText(renderer, _("Achievements and Statistics"), objThemes.getTextColor(false), SCREEN_WIDTH);
//Create GUI.
createGUI(imageManager, renderer);
}
//Destructor.
StatisticsScreen::~StatisticsScreen(){
//Delete the GUI.
if(GUIObjectRoot){
delete GUIObjectRoot;
GUIObjectRoot=NULL;
}
}
//we are so lazy that we just use height of the first text, ignore the others
#define DRAW_PLAYER_STATISTICS(name,var,fmt) { \
SurfacePtr surface(TTF_RenderUTF8_Blended(fontGUISmall,name,objThemes.getTextColor(true))); \
SurfacePtr stats = createSurface(w,surface->h); \
SDL_FillRect(stats.get(),NULL,-1); \
applySurface(4,0,surface.get(),stats.get(),NULL); \
y=surface->h; \
- SDL_snprintf(formatString.data(),formatString.size(),fmt,statsMgr.player##var+statsMgr.shadow##var); \
- surface.reset(TTF_RenderUTF8_Blended(fontText,formatString.data(),objThemes.getTextColor(true))); \
+ surface.reset(TTF_RenderUTF8_Blended(fontText, \
+ tfm::format(fmt,statsMgr.player##var+statsMgr.shadow##var).c_str(), \
+ objThemes.getTextColor(true))); \
applySurface(w-260-surface->w,(y-surface->h)/2,surface.get(),stats.get(),NULL); \
- SDL_snprintf(formatString.data(),formatString.size(),fmt,statsMgr.player##var); \
- surface.reset(TTF_RenderUTF8_Blended(fontText,formatString.data(),objThemes.getTextColor(true))); \
+ surface.reset(TTF_RenderUTF8_Blended(fontText, \
+ tfm::format(fmt,statsMgr.player##var).c_str(), \
+ objThemes.getTextColor(true))); \
applySurface(w-140-surface->w,(y-surface->h)/2,surface.get(),stats.get(),NULL); \
- SDL_snprintf(formatString.data(),formatString.size(),fmt,statsMgr.shadow##var); \
- surface.reset(TTF_RenderUTF8_Blended(fontText,formatString.data(),objThemes.getTextColor(true))); \
+ surface.reset(TTF_RenderUTF8_Blended(fontText, \
+ tfm::format(fmt,statsMgr.shadow##var).c_str(), \
+ objThemes.getTextColor(true))); \
applySurface(w-20-surface->w,(y-surface->h)/2,surface.get(),stats.get(),NULL); \
list->addItem(renderer,"",textureFromSurface(renderer, std::move(stats))); /* add it to list box */ \
}
//Add an item to the listbox, that displays "name1", and "var1" formatted with "format"
//we are so lazy that we just use height of the first text, ignore the others
template <class T1>
static void drawMiscStatistics1(SDL_Renderer& renderer, int w,GUIListBox *list,const char* name1,const T1 var1,const char* format1){
//create new surface
SurfacePtr nameSurface(TTF_RenderUTF8_Blended(fontGUISmall,name1,objThemes.getTextColor(true)));
SurfacePtr stats=createSurface(w, nameSurface->h);
SDL_FillRect(stats.get(),NULL,-1);
applySurface(4,0,nameSurface.get(),stats.get(),NULL);
const int x=nameSurface->w+8;
const int y=nameSurface->h;
- //draw value
- //char s[1024];
- std::array<char, 1024> s;
- SDL_snprintf(s.data(),s.size(),format1,var1);
- SurfacePtr formatSurface(TTF_RenderUTF8_Blended(fontText,s.data(),objThemes.getTextColor(true)));
+ SurfacePtr formatSurface(TTF_RenderUTF8_Blended(fontText,
+ tfm::format(format1,var1).c_str(),
+ objThemes.getTextColor(true)));
//NOTE: SDL2 port. Not halving the y value here as this ends up looking better.
applySurface(x,y-formatSurface->h,formatSurface.get(),stats.get(),NULL);
//add it to list box
list->addItem(renderer, "",textureFromSurface(renderer, std::move(stats)));
//over
//return stats;
}
//NOTE: Disabled this for the SDL2 port for now. It looks a bit off anyhow.
//Might want to make a more general method that draws as many "cells" as there is space.
//Draws two stats on one line if there is space.
//we are so lazy that we just use height of the first text, ignore the others
/*template <class T1,class T2>
static void drawMiscStatistics2(int w,GUIListBox *list,const char* name1,const T1 var1,const char* format1,const char* name2,const T2 var2,const char* format2){
SDL_Surface* stats=drawMiscStatistics1(w,list,name1,var1,format1);
//Check if the width is enough
if(w>=800){
//draw name
SDL_Surface* surface=TTF_RenderUTF8_Blended(fontGUISmall,name2,objThemes.getTextColor(true));
applySurface(w/2-8,stats->h-surface->h,surface,stats,NULL);
int x=surface->w+w/2;
SDL_FreeSurface(surface);
//draw value
char s[1024];
- sprintf(s,format2,var2);
+ //FIXME: Use tfm::format instead of sprintf to enable locale support
+ FIXME_sprintf(s,format2,var2);
surface=TTF_RenderUTF8_Blended(fontText,s,objThemes.getTextColor(true));
applySurface(x,(stats->h-surface->h)/2,surface,stats,NULL);
SDL_FreeSurface(surface);
}else{
//Split into two rows
drawMiscStatistics1(w,list,name2,var2,format2);
}
}*/
void StatisticsScreen::addAchievements(ImageManager& imageManager, SDL_Renderer &renderer, GUIListBox *list, bool revealUnknownAchievements) {
for (int idx = 0; achievementList[idx].id != NULL; ++idx) {
time_t *lpt = NULL;
map<string, OwnedAchievement>::iterator it = statsMgr.achievements.find(achievementList[idx].id);
if (it != statsMgr.achievements.end()) {
lpt = &it->second.achievedTime;
}
AchievementInfo info = achievementList[idx];
if (revealUnknownAchievements) {
if (info.displayStyle == ACHIEVEMENT_HIDDEN || info.displayStyle == ACHIEVEMENT_TITLE) {
info.displayStyle = ACHIEVEMENT_ALL;
}
}
SDL_Rect r;
r.x = r.y = 0;
r.w = list->width - 16;
auto surface = statsMgr.createAchievementSurface(renderer, &info, &r, false, lpt);
if (surface){
list->addItem(renderer, "", surface);
}
}
}
//Method that will create the GUI.
void StatisticsScreen::createGUI(ImageManager& imageManager, SDL_Renderer &renderer){
//Create the root element of the GUI.
if(GUIObjectRoot){
delete GUIObjectRoot;
GUIObjectRoot=NULL;
}
GUIObjectRoot=new GUIObject(imageManager,renderer,0,0,SCREEN_WIDTH,SCREEN_HEIGHT);
//Create back button.
GUIObject* obj=new GUIButton(imageManager,renderer,SCREEN_WIDTH*0.5,SCREEN_HEIGHT-60,-1,36,_("Back"),0,true,true,GUIGravityCenter);
obj->name="cmdBack";
obj->eventCallback=this;
GUIObjectRoot->addChild(obj);
//Create list box.
listBox=new GUISingleLineListBox(imageManager,renderer,(SCREEN_WIDTH-500)/2,104,500,32);
listBox->addItem(_("Achievements"));
listBox->addItem(_("Statistics"));
listBox->value=0;
GUIObjectRoot->addChild(listBox);
//Create list box for achievements.
GUIListBox *list=new GUIListBox(imageManager,renderer,64,150,SCREEN_WIDTH-128,SCREEN_HEIGHT-150-72);
list->selectable=false;
GUIObjectRoot->addChild(list);
lists.clear();
lists.push_back(list);
addAchievements(imageManager, renderer, list);
//Now create list box for statistics.
list=new GUIListBox(imageManager,renderer,64,150,SCREEN_WIDTH-128,SCREEN_HEIGHT-150-72,true,false);
list->selectable=false;
GUIObjectRoot->addChild(list);
lists.push_back(list);
//Load needed pictures.
//FIXME: hard-coded image path
//TODO: Might want to consider not caching these as most other stuff use textures now.
SDL_Surface* bmPlayer=imageManager.loadImage(getDataPath()+"themes/Cloudscape/characters/player.png");
SDL_Surface* bmShadow=imageManager.loadImage(getDataPath()+"themes/Cloudscape/characters/shadow.png");
SDL_Surface* bmMedal=imageManager.loadImage(getDataPath()+"gfx/medals.png");
- //Render stats.
- //char s[64],s2[64];
- std::array<char, 64> formatString;
-
SDL_Rect r;
int x,y,w=SCREEN_WIDTH-128;
SharedTexture h_bar = [&](){
//The horizontal bar.
SurfacePtr h_bar(createSurface(w,2));
SDL_Color c = objThemes.getTextColor(true);
Uint32 clr=SDL_MapRGB(h_bar->format,c.r,c.g,c.b);
SDL_FillRect(h_bar.get(),NULL,clr);
return textureFromSurface(renderer, std::move(h_bar));
}();
//Player and shadow specific statistics
//The header.
{
SurfacePtr stats = createSurface(w, 44);
SDL_FillRect(stats.get(),NULL,-1);
SurfacePtr surface(TTF_RenderUTF8_Blended(fontGUISmall,_("Total"),objThemes.getTextColor(true)));
applySurface(w-260-surface->w,stats->h-surface->h,surface.get(),stats.get(),NULL);
//FIXME: hard-coded player and shadow images
r.x=0;r.y=0;r.w=23;r.h=40;
applySurface(w-140-r.w,stats.get()->h-40,bmPlayer,stats.get(),&r);
applySurface(w-20-r.w,stats.get()->h-40,bmShadow,stats.get(),&r);
list->addItem(renderer, "",textureFromSurface(renderer, std::move(stats)));
}
//Each items.
{
DRAW_PLAYER_STATISTICS(_("Traveling distance (m)"),TravelingDistance,"%0.1f");
DRAW_PLAYER_STATISTICS(_("Jump times"),Jumps,"%d");
DRAW_PLAYER_STATISTICS(_("Die times"),Dies,"%d");
DRAW_PLAYER_STATISTICS(_("Squashed times"),Squashed,"%d");
}
//Game specific statistics.
list->addItem(renderer, "",h_bar);
auto drawMiscStats = [&](const char* name1,const int var1,const char* format1) {
drawMiscStatistics1(renderer, w, list, name1, var1, format1);
};
drawMiscStats(_("Recordings:"),statsMgr.recordTimes,"%d");
drawMiscStats(_("Switch pulled times:"),statsMgr.switchTimes,"%d");
drawMiscStats(_("Swap times:"),statsMgr.swapTimes,"%d");
drawMiscStats(_("Save times:"),statsMgr.saveTimes,"%d");
drawMiscStats(_("Load times:"),statsMgr.loadTimes,"%d");
//Level specific statistics
list->addItem(renderer, "",h_bar);
{
SurfacePtr surface(TTF_RenderUTF8_Blended(fontGUISmall,_("Completed levels:"),objThemes.getTextColor(true)));
SurfacePtr stats = createSurface(w, surface->h);
SDL_FillRect(stats.get(),NULL,-1);
applySurface(4,0,surface.get(),stats.get(),NULL);
x=surface->w+8;
y=surface->h;
- SDL_snprintf(formatString.data(), formatString.size(),"%d",statsMgr.completedLevels);
- surface.reset(TTF_RenderUTF8_Blended(fontText,formatString.data(),objThemes.getTextColor(true)));
+ surface.reset(TTF_RenderUTF8_Blended(fontText,
+ tfm::format("%d", statsMgr.completedLevels).c_str(),
+ objThemes.getTextColor(true)));
applySurface(x,(y-surface->h),surface.get(),stats.get(),NULL);
- SDL_snprintf(formatString.data(), formatString.size(),"%d",statsMgr.completedLevels-statsMgr.goldLevels-statsMgr.silverLevels);
- surface.reset(TTF_RenderUTF8_Blended(fontText,formatString.data(),objThemes.getTextColor(true)));
+ surface.reset(TTF_RenderUTF8_Blended(fontText,
+ tfm::format("%d", statsMgr.completedLevels - statsMgr.goldLevels - statsMgr.silverLevels).c_str(),
+ objThemes.getTextColor(true)));
applySurface(w-260-surface->w,(y-surface->h)/2,surface.get(),stats.get(),NULL);
r.x=0;r.y=0;r.w=30;r.h=30;
applySurface(w-260-surface->w-30,(y-30)/2,bmMedal,stats.get(),&r);
- SDL_snprintf(formatString.data(), formatString.size(),"%d",statsMgr.silverLevels);
- surface.reset(TTF_RenderUTF8_Blended(fontText,formatString.data(),objThemes.getTextColor(true)));
+ surface.reset(TTF_RenderUTF8_Blended(fontText,
+ tfm::format("%d", statsMgr.silverLevels).c_str(),
+ objThemes.getTextColor(true)));
applySurface(w-140-surface->w,(y-surface->h)/2,surface.get(),stats.get(),NULL);
r.x+=30;
applySurface(w-140-surface->w-30,(y-30)/2,bmMedal,stats.get(),&r);
- SDL_snprintf(formatString.data(), formatString.size(),"%d",statsMgr.goldLevels);
- surface.reset(TTF_RenderUTF8_Blended(fontText,formatString.data(),objThemes.getTextColor(true)));
+ surface.reset(TTF_RenderUTF8_Blended(fontText,
+ tfm::format("%d", statsMgr.goldLevels).c_str(),
+ objThemes.getTextColor(true)));
applySurface(w-20-surface->w,(y-surface->h)/2,surface.get(),stats.get(),NULL);
r.x+=30;
applySurface(w-20-surface->w-30,(y-30)/2,bmMedal,stats.get(),&r);
list->addItem(renderer,"",textureFromSurface(renderer, std::move(stats)));
}
//Other statistics.
list->addItem(renderer, "",h_bar);
- SDL_snprintf(formatString.data(), formatString.size(),"%02d:%02d:%02d",statsMgr.playTime/3600,(statsMgr.playTime/60)%60,statsMgr.playTime%60);
- drawMiscStatistics1(renderer,w,list,_("In-game time:"),formatString.data(),"%s");
- SDL_snprintf(formatString.data(), formatString.size(),"%02d:%02d:%02d",statsMgr.levelEditTime/3600,(statsMgr.levelEditTime/60)%60,statsMgr.levelEditTime%60);
- drawMiscStatistics1(renderer,w,list,_("Level editing time:"),formatString.data(),"%s");
+ drawMiscStatistics1(renderer,w,list,_("In-game time:"),
+ tfm::format("%02d:%02d:%02d", statsMgr.playTime / 3600, (statsMgr.playTime / 60) % 60, statsMgr.playTime % 60),
+ "%s");
+ drawMiscStatistics1(renderer,w,list,_("Level editing time:"),
+ tfm::format("%02d:%02d:%02d", statsMgr.levelEditTime / 3600, (statsMgr.levelEditTime / 60) % 60, statsMgr.levelEditTime % 60),
+ "%s");
drawMiscStats(_("Created levels:"),statsMgr.createdLevels,"%d");
}
//In this method all the key and mouse events should be handled.
//NOTE: The GUIEvents won't be handled here.
void StatisticsScreen::handleEvents(ImageManager& imageManager, SDL_Renderer& renderer){
//Check if we need to quit, if so enter the exit state.
if(event.type==SDL_QUIT){
setNextState(STATE_EXIT);
}
//Check horizontal movement
int value = listBox->value;
if (inputMgr.isKeyDownEvent(INPUTMGR_RIGHT)){
isKeyboardOnly = true;
value++;
if (value >= (int)listBox->item.size()) value = 0;
} else if (inputMgr.isKeyDownEvent(INPUTMGR_LEFT)){
isKeyboardOnly = true;
value--;
if (value < 0) value = listBox->item.size() - 1;
}
listBox->value = value;
//Check vertical movement
if (value >= 0 && value < (int)lists.size()) {
if (inputMgr.isKeyDownEvent(INPUTMGR_UP)){
isKeyboardOnly = true;
lists[value]->scrollScrollbar(-1);
} else if (inputMgr.isKeyDownEvent(INPUTMGR_DOWN)){
isKeyboardOnly = true;
lists[value]->scrollScrollbar(1);
}
}
//Yet another cheat "ls -la" which reveals all unknown achievements
static char input[6];
static int inputLen = 0;
if (value == 0) {
if (event.type == SDL_KEYDOWN) {
if (event.key.keysym.sym >= 32 && event.key.keysym.sym <= 126) {
if (inputLen < sizeof(input)) input[inputLen] = event.key.keysym.sym;
inputLen++;
} else {
if (event.key.keysym.sym == SDLK_RETURN && inputLen == 6 &&
input[0] == 'l' && input[1] == 's' && input[2] == ' ' && input[3] == '-' && input[4] == 'l' && input[5] == 'a')
{
if (easterEggScreen(imageManager, renderer)) {
//new achievement
statsMgr.newAchievement("cheat");
//reload achievement list with hidden achievements revealed
lists[0]->clearItems();
addAchievements(imageManager, renderer, lists[0], true);
}
}
inputLen = 0;
}
}
} else {
inputLen = 0;
}
//Check if the escape button is pressed, if so go back to the main menu.
if(inputMgr.isKeyDownEvent(INPUTMGR_ESCAPE)){
setNextState(STATE_MENU);
}
}
//All the logic that needs to be done should go in this method.
void StatisticsScreen::logic(ImageManager&, SDL_Renderer&){
}
//This method handles all the rendering.
void StatisticsScreen::render(ImageManager&, SDL_Renderer& renderer){
//Draw background.
objThemes.getBackground(true)->draw(renderer);
objThemes.getBackground(true)->updateAnimation();
//Draw title.
drawTitleTexture(SCREEN_WIDTH, *title, renderer);
//Draw statistics.
int value=listBox->value;
for(unsigned int i=0;i<lists.size();i++){
lists[i]->visible=(i==value);
}
}
//Method that will be called when the screen size has been changed in runtime.
void StatisticsScreen::resize(ImageManager &imageManager, SDL_Renderer &renderer){
//Recreate the gui to fit the new resolution.
createGUI(imageManager, renderer);
}
diff --git a/src/libs/tinyformat/tinyformat.cpp b/src/libs/tinyformat/tinyformat.cpp
new file mode 100644
index 0000000..6392833
--- /dev/null
+++ b/src/libs/tinyformat/tinyformat.cpp
@@ -0,0 +1,21 @@
+#include "tinyformat.h"
+#include <locale>
+
+namespace tinyformat {
+
+std::locale locale = std::locale::classic();
+
+void setLocale(const char* std_name) {
+ try {
+ tfm::locale = std::locale(std_name);
+ } catch (...) {
+ std::cerr << "ERROR: Failed to create std::locale for '" << std_name << "'!" << std::endl;
+ tfm::locale = std::locale::classic();
+ }
+}
+
+void imbue(std::ostream& o) {
+ o.imbue(tfm::locale);
+}
+
+}
diff --git a/src/libs/tinyformat/tinyformat.h b/src/libs/tinyformat/tinyformat.h
index 0449875..aa95a01 100644
--- a/src/libs/tinyformat/tinyformat.h
+++ b/src/libs/tinyformat/tinyformat.h
@@ -1,1039 +1,1050 @@
// tinyformat.h
// Copyright (C) 2011, Chris Foster [chris42f (at) gmail (d0t) com]
//
// Boost Software License - Version 1.0
//
// Permission is hereby granted, free of charge, to any person or organization
// obtaining a copy of the software and accompanying documentation covered by
// this license (the "Software") to use, reproduce, display, distribute,
// execute, and transmit the Software, and to prepare derivative works of the
// Software, and to permit third-parties to whom the Software is furnished to
// do so, all subject to the following:
//
// The copyright notices in the Software and this entire statement, including
// the above license grant, this restriction and the following disclaimer,
// must be included in all copies of the Software, in whole or in part, and
// all derivative works of the Software, unless such copies or derivative
// works are solely in the form of machine-executable object code generated by
// a source language processor.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
// SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
// FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
//------------------------------------------------------------------------------
// Tinyformat: A minimal type safe printf replacement
//
// tinyformat.h is a type safe printf replacement library in a single C++
// header file. Design goals include:
//
// * Type safety and extensibility for user defined types.
// * C99 printf() compatibility, to the extent possible using std::ostream
// * Simplicity and minimalism. A single header file to include and distribute
// with your projects.
// * Augment rather than replace the standard stream formatting mechanism
// * C++98 support, with optional C++11 niceties
//
//
// Main interface example usage
// ----------------------------
//
// To print a date to std::cout:
//
// std::string weekday = "Wednesday";
// const char* month = "July";
// size_t day = 27;
// long hour = 14;
// int min = 44;
//
// tfm::printf("%s, %s %d, %.2d:%.2d\n", weekday, month, day, hour, min);
//
// The strange types here emphasize the type safety of the interface; it is
// possible to print a std::string using the "%s" conversion, and a
// size_t using the "%d" conversion. A similar result could be achieved
// using either of the tfm::format() functions. One prints on a user provided
// stream:
//
// tfm::format(std::cerr, "%s, %s %d, %.2d:%.2d\n",
// weekday, month, day, hour, min);
//
// The other returns a std::string:
//
// std::string date = tfm::format("%s, %s %d, %.2d:%.2d\n",
// weekday, month, day, hour, min);
// std::cout << date;
//
// These are the three primary interface functions. There is also a
// convenience function printfln() which appends a newline to the usual result
// of printf() for super simple logging.
//
//
// User defined format functions
// -----------------------------
//
// Simulating variadic templates in C++98 is pretty painful since it requires
// writing out the same function for each desired number of arguments. To make
// this bearable tinyformat comes with a set of macros which are used
// internally to generate the API, but which may also be used in user code.
//
// The three macros TINYFORMAT_ARGTYPES(n), TINYFORMAT_VARARGS(n) and
// TINYFORMAT_PASSARGS(n) will generate a list of n argument types,
// type/name pairs and argument names respectively when called with an integer
// n between 1 and 16. We can use these to define a macro which generates the
// desired user defined function with n arguments. To generate all 16 user
// defined function bodies, use the macro TINYFORMAT_FOREACH_ARGNUM. For an
// example, see the implementation of printf() at the end of the source file.
//
// Sometimes it's useful to be able to pass a list of format arguments through
// to a non-template function. The FormatList class is provided as a way to do
// this by storing the argument list in a type-opaque way. Continuing the
// example from above, we construct a FormatList using makeFormatList():
//
// FormatListRef formatList = tfm::makeFormatList(weekday, month, day, hour, min);
//
// The format list can now be passed into any non-template function and used
// via a call to the vformat() function:
//
// tfm::vformat(std::cout, "%s, %s %d, %.2d:%.2d\n", formatList);
//
//
// Additional API information
// --------------------------
//
// Error handling: Define TINYFORMAT_ERROR to customize the error handling for
// format strings which are unsupported or have the wrong number of format
// specifiers (calls assert() by default).
//
// User defined types: Uses operator<< for user defined types by default.
// Overload formatValue() for more control.
#ifndef TINYFORMAT_H_INCLUDED
#define TINYFORMAT_H_INCLUDED
namespace tinyformat {}
//------------------------------------------------------------------------------
// Config section. Customize to your liking!
// Namespace alias to encourage brevity
namespace tfm = tinyformat;
// Error handling; calls assert() by default.
// #define TINYFORMAT_ERROR(reasonString) your_error_handler(reasonString)
// Define for C++11 variadic templates which make the code shorter & more
// general. If you don't define this, C++11 support is autodetected below.
// #define TINYFORMAT_USE_VARIADIC_TEMPLATES
//------------------------------------------------------------------------------
// Implementation details.
#include <algorithm>
#include <cassert>
#include <iostream>
#include <sstream>
#ifndef TINYFORMAT_ERROR
# define TINYFORMAT_ERROR(reason) assert(0 && reason)
#endif
#if !defined(TINYFORMAT_USE_VARIADIC_TEMPLATES) && !defined(TINYFORMAT_NO_VARIADIC_TEMPLATES)
# ifdef __GXX_EXPERIMENTAL_CXX0X__
# define TINYFORMAT_USE_VARIADIC_TEMPLATES
# endif
#endif
#if defined(__GLIBCXX__) && __GLIBCXX__ < 20080201
// std::showpos is broken on old libstdc++ as provided with OSX. See
// http://gcc.gnu.org/ml/libstdc++/2007-11/msg00075.html
# define TINYFORMAT_OLD_LIBSTDCPLUSPLUS_WORKAROUND
#endif
#ifdef __APPLE__
// Workaround OSX linker warning: xcode uses different default symbol
// visibilities for static libs vs executables (see issue #25)
# define TINYFORMAT_HIDDEN __attribute__((visibility("hidden")))
#else
# define TINYFORMAT_HIDDEN
#endif
namespace tinyformat {
+// Set the locale for tinyformat which is mainly used to format float numbers.
+void setLocale(const char* std_name);
+
+// Internal function.
+void imbue(std::ostream& o);
+
//------------------------------------------------------------------------------
namespace detail {
// Test whether type T1 is convertible to type T2
template <typename T1, typename T2>
struct is_convertible
{
private:
// two types of different size
struct fail { char dummy[2]; };
struct succeed { char dummy; };
// Try to convert a T1 to a T2 by plugging into tryConvert
static fail tryConvert(...);
static succeed tryConvert(const T2&);
static const T1& makeT1();
public:
# ifdef _MSC_VER
// Disable spurious loss of precision warnings in tryConvert(makeT1())
# pragma warning(push)
# pragma warning(disable:4244)
# pragma warning(disable:4267)
# endif
// Standard trick: the (...) version of tryConvert will be chosen from
// the overload set only if the version taking a T2 doesn't match.
// Then we compare the sizes of the return types to check which
// function matched. Very neat, in a disgusting kind of way :)
static const bool value =
sizeof(tryConvert(makeT1())) == sizeof(succeed);
# ifdef _MSC_VER
# pragma warning(pop)
# endif
};
// Detect when a type is not a wchar_t string
template<typename T> struct is_wchar { typedef int tinyformat_wchar_is_not_supported; };
template<> struct is_wchar<wchar_t*> {};
template<> struct is_wchar<const wchar_t*> {};
template<int n> struct is_wchar<const wchar_t[n]> {};
template<int n> struct is_wchar<wchar_t[n]> {};
// Format the value by casting to type fmtT. This default implementation
// should never be called.
template<typename T, typename fmtT, bool convertible = is_convertible<T, fmtT>::value>
struct formatValueAsType
{
static void invoke(std::ostream& /*out*/, const T& /*value*/) { assert(0); }
};
// Specialized version for types that can actually be converted to fmtT, as
// indicated by the "convertible" template parameter.
template<typename T, typename fmtT>
struct formatValueAsType<T,fmtT,true>
{
static void invoke(std::ostream& out, const T& value)
{ out << static_cast<fmtT>(value); }
};
#ifdef TINYFORMAT_OLD_LIBSTDCPLUSPLUS_WORKAROUND
template<typename T, bool convertible = is_convertible<T, int>::value>
struct formatZeroIntegerWorkaround
{
static bool invoke(std::ostream& /**/, const T& /**/) { return false; }
};
template<typename T>
struct formatZeroIntegerWorkaround<T,true>
{
static bool invoke(std::ostream& out, const T& value)
{
if (static_cast<int>(value) == 0 && out.flags() & std::ios::showpos)
{
out << "+0";
return true;
}
return false;
}
};
#endif // TINYFORMAT_OLD_LIBSTDCPLUSPLUS_WORKAROUND
// Convert an arbitrary type to integer. The version with convertible=false
// throws an error.
template<typename T, bool convertible = is_convertible<T,int>::value>
struct convertToInt
{
static int invoke(const T& /*value*/)
{
TINYFORMAT_ERROR("tinyformat: Cannot convert from argument type to "
"integer for use as variable width or precision");
return 0;
}
};
// Specialization for convertToInt when conversion is possible
template<typename T>
struct convertToInt<T,true>
{
static int invoke(const T& value) { return static_cast<int>(value); }
};
// Format at most ntrunc characters to the given stream.
template<typename T>
inline void formatTruncated(std::ostream& out, const T& value, int ntrunc)
{
std::ostringstream tmp;
+ tfm::imbue(tmp);
tmp << value;
std::string result = tmp.str();
out.write(result.c_str(), (std::min)(ntrunc, static_cast<int>(result.size())));
}
#define TINYFORMAT_DEFINE_FORMAT_TRUNCATED_CSTR(type) \
inline void formatTruncated(std::ostream& out, type* value, int ntrunc) \
{ \
std::streamsize len = 0; \
while(len < ntrunc && value[len] != 0) \
++len; \
out.write(value, len); \
}
// Overload for const char* and char*. Could overload for signed & unsigned
// char too, but these are technically unneeded for printf compatibility.
TINYFORMAT_DEFINE_FORMAT_TRUNCATED_CSTR(const char)
TINYFORMAT_DEFINE_FORMAT_TRUNCATED_CSTR(char)
#undef TINYFORMAT_DEFINE_FORMAT_TRUNCATED_CSTR
} // namespace detail
//------------------------------------------------------------------------------
// Variable formatting functions. May be overridden for user-defined types if
// desired.
/// Format a value into a stream, delegating to operator<< by default.
///
/// Users may override this for their own types. When this function is called,
/// the stream flags will have been modified according to the format string.
/// The format specification is provided in the range [fmtBegin, fmtEnd). For
/// truncating conversions, ntrunc is set to the desired maximum number of
/// characters, for example "%.7s" calls formatValue with ntrunc = 7.
///
/// By default, formatValue() uses the usual stream insertion operator
/// operator<< to format the type T, with special cases for the %c and %p
/// conversions.
template<typename T>
inline void formatValue(std::ostream& out, const char* /*fmtBegin*/,
const char* fmtEnd, int ntrunc, const T& value)
{
#ifndef TINYFORMAT_ALLOW_WCHAR_STRINGS
// Since we don't support printing of wchar_t using "%ls", make it fail at
// compile time in preference to printing as a void* at runtime.
typedef typename detail::is_wchar<T>::tinyformat_wchar_is_not_supported DummyType;
(void) DummyType(); // avoid unused type warning with gcc-4.8
#endif
// The mess here is to support the %c and %p conversions: if these
// conversions are active we try to convert the type to a char or const
// void* respectively and format that instead of the value itself. For the
// %p conversion it's important to avoid dereferencing the pointer, which
// could otherwise lead to a crash when printing a dangling (const char*).
const bool canConvertToChar = detail::is_convertible<T,char>::value;
const bool canConvertToVoidPtr = detail::is_convertible<T, const void*>::value;
if(canConvertToChar && *(fmtEnd-1) == 'c')
detail::formatValueAsType<T, char>::invoke(out, value);
else if(canConvertToVoidPtr && *(fmtEnd-1) == 'p')
detail::formatValueAsType<T, const void*>::invoke(out, value);
#ifdef TINYFORMAT_OLD_LIBSTDCPLUSPLUS_WORKAROUND
else if(detail::formatZeroIntegerWorkaround<T>::invoke(out, value)) /**/;
#endif
else if(ntrunc >= 0)
{
// Take care not to overread C strings in truncating conversions like
// "%.4s" where at most 4 characters may be read.
detail::formatTruncated(out, value, ntrunc);
}
else
out << value;
}
// Overloaded version for char types to support printing as an integer
#define TINYFORMAT_DEFINE_FORMATVALUE_CHAR(charType) \
inline void formatValue(std::ostream& out, const char* /*fmtBegin*/, \
const char* fmtEnd, int /**/, charType value) \
{ \
switch(*(fmtEnd-1)) \
{ \
case 'u': case 'd': case 'i': case 'o': case 'X': case 'x': \
out << static_cast<int>(value); break; \
default: \
out << value; break; \
} \
}
// per 3.9.1: char, signed char and unsigned char are all distinct types
TINYFORMAT_DEFINE_FORMATVALUE_CHAR(char)
TINYFORMAT_DEFINE_FORMATVALUE_CHAR(signed char)
TINYFORMAT_DEFINE_FORMATVALUE_CHAR(unsigned char)
#undef TINYFORMAT_DEFINE_FORMATVALUE_CHAR
//------------------------------------------------------------------------------
// Tools for emulating variadic templates in C++98. The basic idea here is
// stolen from the boost preprocessor metaprogramming library and cut down to
// be just general enough for what we need.
#define TINYFORMAT_ARGTYPES(n) TINYFORMAT_ARGTYPES_ ## n
#define TINYFORMAT_VARARGS(n) TINYFORMAT_VARARGS_ ## n
#define TINYFORMAT_PASSARGS(n) TINYFORMAT_PASSARGS_ ## n
#define TINYFORMAT_PASSARGS_TAIL(n) TINYFORMAT_PASSARGS_TAIL_ ## n
// To keep it as transparent as possible, the macros below have been generated
// using python via the excellent cog.py code generation script. This avoids
// the need for a bunch of complex (but more general) preprocessor tricks as
// used in boost.preprocessor.
//
// To rerun the code generation in place, use `cog.py -r tinyformat.h`
// (see http://nedbatchelder.com/code/cog). Alternatively you can just create
// extra versions by hand.
/*[[[cog
maxParams = 16
def makeCommaSepLists(lineTemplate, elemTemplate, startInd=1):
for j in range(startInd,maxParams+1):
list = ', '.join([elemTemplate % {'i':i} for i in range(startInd,j+1)])
cog.outl(lineTemplate % {'j':j, 'list':list})
makeCommaSepLists('#define TINYFORMAT_ARGTYPES_%(j)d %(list)s',
'class T%(i)d')
cog.outl()
makeCommaSepLists('#define TINYFORMAT_VARARGS_%(j)d %(list)s',
'const T%(i)d& v%(i)d')
cog.outl()
makeCommaSepLists('#define TINYFORMAT_PASSARGS_%(j)d %(list)s', 'v%(i)d')
cog.outl()
cog.outl('#define TINYFORMAT_PASSARGS_TAIL_1')
makeCommaSepLists('#define TINYFORMAT_PASSARGS_TAIL_%(j)d , %(list)s',
'v%(i)d', startInd = 2)
cog.outl()
cog.outl('#define TINYFORMAT_FOREACH_ARGNUM(m) \\\n ' +
' '.join(['m(%d)' % (j,) for j in range(1,maxParams+1)]))
]]]*/
#define TINYFORMAT_ARGTYPES_1 class T1
#define TINYFORMAT_ARGTYPES_2 class T1, class T2
#define TINYFORMAT_ARGTYPES_3 class T1, class T2, class T3
#define TINYFORMAT_ARGTYPES_4 class T1, class T2, class T3, class T4
#define TINYFORMAT_ARGTYPES_5 class T1, class T2, class T3, class T4, class T5
#define TINYFORMAT_ARGTYPES_6 class T1, class T2, class T3, class T4, class T5, class T6
#define TINYFORMAT_ARGTYPES_7 class T1, class T2, class T3, class T4, class T5, class T6, class T7
#define TINYFORMAT_ARGTYPES_8 class T1, class T2, class T3, class T4, class T5, class T6, class T7, class T8
#define TINYFORMAT_ARGTYPES_9 class T1, class T2, class T3, class T4, class T5, class T6, class T7, class T8, class T9
#define TINYFORMAT_ARGTYPES_10 class T1, class T2, class T3, class T4, class T5, class T6, class T7, class T8, class T9, class T10
#define TINYFORMAT_ARGTYPES_11 class T1, class T2, class T3, class T4, class T5, class T6, class T7, class T8, class T9, class T10, class T11
#define TINYFORMAT_ARGTYPES_12 class T1, class T2, class T3, class T4, class T5, class T6, class T7, class T8, class T9, class T10, class T11, class T12
#define TINYFORMAT_ARGTYPES_13 class T1, class T2, class T3, class T4, class T5, class T6, class T7, class T8, class T9, class T10, class T11, class T12, class T13
#define TINYFORMAT_ARGTYPES_14 class T1, class T2, class T3, class T4, class T5, class T6, class T7, class T8, class T9, class T10, class T11, class T12, class T13, class T14
#define TINYFORMAT_ARGTYPES_15 class T1, class T2, class T3, class T4, class T5, class T6, class T7, class T8, class T9, class T10, class T11, class T12, class T13, class T14, class T15
#define TINYFORMAT_ARGTYPES_16 class T1, class T2, class T3, class T4, class T5, class T6, class T7, class T8, class T9, class T10, class T11, class T12, class T13, class T14, class T15, class T16
#define TINYFORMAT_VARARGS_1 const T1& v1
#define TINYFORMAT_VARARGS_2 const T1& v1, const T2& v2
#define TINYFORMAT_VARARGS_3 const T1& v1, const T2& v2, const T3& v3
#define TINYFORMAT_VARARGS_4 const T1& v1, const T2& v2, const T3& v3, const T4& v4
#define TINYFORMAT_VARARGS_5 const T1& v1, const T2& v2, const T3& v3, const T4& v4, const T5& v5
#define TINYFORMAT_VARARGS_6 const T1& v1, const T2& v2, const T3& v3, const T4& v4, const T5& v5, const T6& v6
#define TINYFORMAT_VARARGS_7 const T1& v1, const T2& v2, const T3& v3, const T4& v4, const T5& v5, const T6& v6, const T7& v7
#define TINYFORMAT_VARARGS_8 const T1& v1, const T2& v2, const T3& v3, const T4& v4, const T5& v5, const T6& v6, const T7& v7, const T8& v8
#define TINYFORMAT_VARARGS_9 const T1& v1, const T2& v2, const T3& v3, const T4& v4, const T5& v5, const T6& v6, const T7& v7, const T8& v8, const T9& v9
#define TINYFORMAT_VARARGS_10 const T1& v1, const T2& v2, const T3& v3, const T4& v4, const T5& v5, const T6& v6, const T7& v7, const T8& v8, const T9& v9, const T10& v10
#define TINYFORMAT_VARARGS_11 const T1& v1, const T2& v2, const T3& v3, const T4& v4, const T5& v5, const T6& v6, const T7& v7, const T8& v8, const T9& v9, const T10& v10, const T11& v11
#define TINYFORMAT_VARARGS_12 const T1& v1, const T2& v2, const T3& v3, const T4& v4, const T5& v5, const T6& v6, const T7& v7, const T8& v8, const T9& v9, const T10& v10, const T11& v11, const T12& v12
#define TINYFORMAT_VARARGS_13 const T1& v1, const T2& v2, const T3& v3, const T4& v4, const T5& v5, const T6& v6, const T7& v7, const T8& v8, const T9& v9, const T10& v10, const T11& v11, const T12& v12, const T13& v13
#define TINYFORMAT_VARARGS_14 const T1& v1, const T2& v2, const T3& v3, const T4& v4, const T5& v5, const T6& v6, const T7& v7, const T8& v8, const T9& v9, const T10& v10, const T11& v11, const T12& v12, const T13& v13, const T14& v14
#define TINYFORMAT_VARARGS_15 const T1& v1, const T2& v2, const T3& v3, const T4& v4, const T5& v5, const T6& v6, const T7& v7, const T8& v8, const T9& v9, const T10& v10, const T11& v11, const T12& v12, const T13& v13, const T14& v14, const T15& v15
#define TINYFORMAT_VARARGS_16 const T1& v1, const T2& v2, const T3& v3, const T4& v4, const T5& v5, const T6& v6, const T7& v7, const T8& v8, const T9& v9, const T10& v10, const T11& v11, const T12& v12, const T13& v13, const T14& v14, const T15& v15, const T16& v16
#define TINYFORMAT_PASSARGS_1 v1
#define TINYFORMAT_PASSARGS_2 v1, v2
#define TINYFORMAT_PASSARGS_3 v1, v2, v3
#define TINYFORMAT_PASSARGS_4 v1, v2, v3, v4
#define TINYFORMAT_PASSARGS_5 v1, v2, v3, v4, v5
#define TINYFORMAT_PASSARGS_6 v1, v2, v3, v4, v5, v6
#define TINYFORMAT_PASSARGS_7 v1, v2, v3, v4, v5, v6, v7
#define TINYFORMAT_PASSARGS_8 v1, v2, v3, v4, v5, v6, v7, v8
#define TINYFORMAT_PASSARGS_9 v1, v2, v3, v4, v5, v6, v7, v8, v9
#define TINYFORMAT_PASSARGS_10 v1, v2, v3, v4, v5, v6, v7, v8, v9, v10
#define TINYFORMAT_PASSARGS_11 v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11
#define TINYFORMAT_PASSARGS_12 v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12
#define TINYFORMAT_PASSARGS_13 v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13
#define TINYFORMAT_PASSARGS_14 v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14
#define TINYFORMAT_PASSARGS_15 v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15
#define TINYFORMAT_PASSARGS_16 v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16
#define TINYFORMAT_PASSARGS_TAIL_1
#define TINYFORMAT_PASSARGS_TAIL_2 , v2
#define TINYFORMAT_PASSARGS_TAIL_3 , v2, v3
#define TINYFORMAT_PASSARGS_TAIL_4 , v2, v3, v4
#define TINYFORMAT_PASSARGS_TAIL_5 , v2, v3, v4, v5
#define TINYFORMAT_PASSARGS_TAIL_6 , v2, v3, v4, v5, v6
#define TINYFORMAT_PASSARGS_TAIL_7 , v2, v3, v4, v5, v6, v7
#define TINYFORMAT_PASSARGS_TAIL_8 , v2, v3, v4, v5, v6, v7, v8
#define TINYFORMAT_PASSARGS_TAIL_9 , v2, v3, v4, v5, v6, v7, v8, v9
#define TINYFORMAT_PASSARGS_TAIL_10 , v2, v3, v4, v5, v6, v7, v8, v9, v10
#define TINYFORMAT_PASSARGS_TAIL_11 , v2, v3, v4, v5, v6, v7, v8, v9, v10, v11
#define TINYFORMAT_PASSARGS_TAIL_12 , v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12
#define TINYFORMAT_PASSARGS_TAIL_13 , v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13
#define TINYFORMAT_PASSARGS_TAIL_14 , v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14
#define TINYFORMAT_PASSARGS_TAIL_15 , v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15
#define TINYFORMAT_PASSARGS_TAIL_16 , v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16
#define TINYFORMAT_FOREACH_ARGNUM(m) \
m(1) m(2) m(3) m(4) m(5) m(6) m(7) m(8) m(9) m(10) m(11) m(12) m(13) m(14) m(15) m(16)
//[[[end]]]
namespace detail {
// Type-opaque holder for an argument to format(), with associated actions on
// the type held as explicit function pointers. This allows FormatArg's for
// each argument to be allocated as a homogenous array inside FormatList
// whereas a naive implementation based on inheritance does not.
class FormatArg
{
public:
FormatArg() {}
template<typename T>
FormatArg(const T& value)
: m_value(static_cast<const void*>(&value)),
m_formatImpl(&formatImpl<T>),
m_toIntImpl(&toIntImpl<T>)
{ }
void format(std::ostream& out, const char* fmtBegin,
const char* fmtEnd, int ntrunc) const
{
m_formatImpl(out, fmtBegin, fmtEnd, ntrunc, m_value);
}
int toInt() const
{
return m_toIntImpl(m_value);
}
private:
template<typename T>
TINYFORMAT_HIDDEN static void formatImpl(std::ostream& out, const char* fmtBegin,
const char* fmtEnd, int ntrunc, const void* value)
{
formatValue(out, fmtBegin, fmtEnd, ntrunc, *static_cast<const T*>(value));
}
template<typename T>
TINYFORMAT_HIDDEN static int toIntImpl(const void* value)
{
return convertToInt<T>::invoke(*static_cast<const T*>(value));
}
const void* m_value;
void (*m_formatImpl)(std::ostream& out, const char* fmtBegin,
const char* fmtEnd, int ntrunc, const void* value);
int (*m_toIntImpl)(const void* value);
};
// Parse and return an integer from the string c, as atoi()
// On return, c is set to one past the end of the integer.
inline int parseIntAndAdvance(const char*& c)
{
int i = 0;
for(;*c >= '0' && *c <= '9'; ++c)
i = 10*i + (*c - '0');
return i;
}
// Print literal part of format string and return next format spec
// position.
//
// Skips over any occurrences of '%%', printing a literal '%' to the
// output. The position of the first % character of the next
// nontrivial format spec is returned, or the end of string.
inline const char* printFormatStringLiteral(std::ostream& out, const char* fmt)
{
const char* c = fmt;
for(;; ++c)
{
switch(*c)
{
case '\0':
out.write(fmt, c - fmt);
return c;
case '%':
out.write(fmt, c - fmt);
if(*(c+1) != '%')
return c;
// for "%%", tack trailing % onto next literal section.
fmt = ++c;
break;
default:
break;
}
}
}
// Parse a format string and set the stream state accordingly.
//
// The format mini-language recognized here is meant to be the one from C99,
// with the form "%[flags][width][.precision][length]type".
//
// Formatting options which can't be natively represented using the ostream
// state are returned in spacePadPositive (for space padded positive numbers)
// and ntrunc (for truncating conversions). argIndex is incremented if
// necessary to pull out variable width and precision . The function returns a
// pointer to the character after the end of the current format spec.
inline const char* streamStateFromFormat(std::ostream& out, bool& spacePadPositive,
int& ntrunc, const char* fmtStart,
const detail::FormatArg* formatters,
int& argIndex, int numFormatters)
{
if(*fmtStart != '%')
{
TINYFORMAT_ERROR("tinyformat: Not enough conversion specifiers in format string");
return fmtStart;
}
// Reset stream state to defaults.
out.width(0);
out.precision(6);
out.fill(' ');
// Reset most flags; ignore irrelevant unitbuf & skipws.
out.unsetf(std::ios::adjustfield | std::ios::basefield |
std::ios::floatfield | std::ios::showbase | std::ios::boolalpha |
std::ios::showpoint | std::ios::showpos | std::ios::uppercase);
bool precisionSet = false;
bool widthSet = false;
int widthExtra = 0;
const char* c = fmtStart + 1;
// 1) Parse flags
for(;; ++c)
{
switch(*c)
{
case '#':
out.setf(std::ios::showpoint | std::ios::showbase);
continue;
case '0':
// overridden by left alignment ('-' flag)
if(!(out.flags() & std::ios::left))
{
// Use internal padding so that numeric values are
// formatted correctly, eg -00010 rather than 000-10
out.fill('0');
out.setf(std::ios::internal, std::ios::adjustfield);
}
continue;
case '-':
out.fill(' ');
out.setf(std::ios::left, std::ios::adjustfield);
continue;
case ' ':
// overridden by show positive sign, '+' flag.
if(!(out.flags() & std::ios::showpos))
spacePadPositive = true;
continue;
case '+':
out.setf(std::ios::showpos);
spacePadPositive = false;
widthExtra = 1;
continue;
default:
break;
}
break;
}
// 2) Parse width
if(*c >= '0' && *c <= '9')
{
widthSet = true;
out.width(parseIntAndAdvance(c));
}
if(*c == '*')
{
widthSet = true;
int width = 0;
if(argIndex < numFormatters)
width = formatters[argIndex++].toInt();
else
TINYFORMAT_ERROR("tinyformat: Not enough arguments to read variable width");
if(width < 0)
{
// negative widths correspond to '-' flag set
out.fill(' ');
out.setf(std::ios::left, std::ios::adjustfield);
width = -width;
}
out.width(width);
++c;
}
// 3) Parse precision
if(*c == '.')
{
++c;
int precision = 0;
if(*c == '*')
{
++c;
if(argIndex < numFormatters)
precision = formatters[argIndex++].toInt();
else
TINYFORMAT_ERROR("tinyformat: Not enough arguments to read variable precision");
}
else
{
if(*c >= '0' && *c <= '9')
precision = parseIntAndAdvance(c);
else if(*c == '-') // negative precisions ignored, treated as zero.
parseIntAndAdvance(++c);
}
out.precision(precision);
precisionSet = true;
}
// 4) Ignore any C99 length modifier
while(*c == 'l' || *c == 'h' || *c == 'L' ||
*c == 'j' || *c == 'z' || *c == 't')
++c;
// 5) We're up to the conversion specifier character.
// Set stream flags based on conversion specifier (thanks to the
// boost::format class for forging the way here).
bool intConversion = false;
switch(*c)
{
case 'u': case 'd': case 'i':
out.setf(std::ios::dec, std::ios::basefield);
intConversion = true;
break;
case 'o':
out.setf(std::ios::oct, std::ios::basefield);
intConversion = true;
break;
case 'X':
out.setf(std::ios::uppercase);
case 'x': case 'p':
out.setf(std::ios::hex, std::ios::basefield);
intConversion = true;
break;
case 'E':
out.setf(std::ios::uppercase);
case 'e':
out.setf(std::ios::scientific, std::ios::floatfield);
out.setf(std::ios::dec, std::ios::basefield);
break;
case 'F':
out.setf(std::ios::uppercase);
case 'f':
out.setf(std::ios::fixed, std::ios::floatfield);
break;
case 'G':
out.setf(std::ios::uppercase);
case 'g':
out.setf(std::ios::dec, std::ios::basefield);
// As in boost::format, let stream decide float format.
out.flags(out.flags() & ~std::ios::floatfield);
break;
case 'a': case 'A':
TINYFORMAT_ERROR("tinyformat: the %a and %A conversion specs "
"are not supported");
break;
case 'c':
// Handled as special case inside formatValue()
break;
case 's':
if(precisionSet)
ntrunc = static_cast<int>(out.precision());
// Make %s print booleans as "true" and "false"
out.setf(std::ios::boolalpha);
break;
case 'n':
// Not supported - will cause problems!
TINYFORMAT_ERROR("tinyformat: %n conversion spec not supported");
break;
case '\0':
TINYFORMAT_ERROR("tinyformat: Conversion spec incorrectly "
"terminated by end of string");
return c;
default:
break;
}
if(intConversion && precisionSet && !widthSet)
{
// "precision" for integers gives the minimum number of digits (to be
// padded with zeros on the left). This isn't really supported by the
// iostreams, but we can approximately simulate it with the width if
// the width isn't otherwise used.
out.width(out.precision() + widthExtra);
out.setf(std::ios::internal, std::ios::adjustfield);
out.fill('0');
}
return c+1;
}
//------------------------------------------------------------------------------
inline void formatImpl(std::ostream& out, const char* fmt,
const detail::FormatArg* formatters,
int numFormatters)
{
// Saved stream state
std::streamsize origWidth = out.width();
std::streamsize origPrecision = out.precision();
std::ios::fmtflags origFlags = out.flags();
char origFill = out.fill();
for (int argIndex = 0; argIndex < numFormatters; ++argIndex)
{
// Parse the format string
fmt = printFormatStringLiteral(out, fmt);
bool spacePadPositive = false;
int ntrunc = -1;
const char* fmtEnd = streamStateFromFormat(out, spacePadPositive, ntrunc, fmt,
formatters, argIndex, numFormatters);
if (argIndex >= numFormatters)
{
// Check args remain after reading any variable width/precision
TINYFORMAT_ERROR("tinyformat: Not enough format arguments");
return;
}
const FormatArg& arg = formatters[argIndex];
// Format the arg into the stream.
if(!spacePadPositive)
arg.format(out, fmt, fmtEnd, ntrunc);
else
{
// The following is a special case with no direct correspondence
// between stream formatting and the printf() behaviour. Simulate
// it crudely by formatting into a temporary string stream and
// munging the resulting string.
std::ostringstream tmpStream;
+ tfm::imbue(tmpStream);
tmpStream.copyfmt(out);
tmpStream.setf(std::ios::showpos);
arg.format(tmpStream, fmt, fmtEnd, ntrunc);
std::string result = tmpStream.str(); // allocates... yuck.
for(size_t i = 0, iend = result.size(); i < iend; ++i)
if(result[i] == '+') result[i] = ' ';
out << result;
}
fmt = fmtEnd;
}
// Print remaining part of format string.
fmt = printFormatStringLiteral(out, fmt);
if(*fmt != '\0')
TINYFORMAT_ERROR("tinyformat: Too many conversion specifiers in format string");
// Restore stream state
out.width(origWidth);
out.precision(origPrecision);
out.flags(origFlags);
out.fill(origFill);
}
} // namespace detail
/// List of template arguments format(), held in a type-opaque way.
///
/// A const reference to FormatList (typedef'd as FormatListRef) may be
/// conveniently used to pass arguments to non-template functions: All type
/// information has been stripped from the arguments, leaving just enough of a
/// common interface to perform formatting as required.
class FormatList
{
public:
FormatList(detail::FormatArg* formatters, int N)
: m_formatters(formatters), m_N(N) { }
friend void vformat(std::ostream& out, const char* fmt,
const FormatList& list);
private:
const detail::FormatArg* m_formatters;
int m_N;
};
/// Reference to type-opaque format list for passing to vformat()
typedef const FormatList& FormatListRef;
namespace detail {
// Format list subclass with fixed storage to avoid dynamic allocation
template<int N>
class FormatListN : public FormatList
{
public:
#ifdef TINYFORMAT_USE_VARIADIC_TEMPLATES
template<typename... Args>
FormatListN(const Args&... args)
: FormatList(&m_formatterStore[0], N),
m_formatterStore { FormatArg(args)... }
{ static_assert(sizeof...(args) == N, "Number of args must be N"); }
#else // C++98 version
void init(int) {}
# define TINYFORMAT_MAKE_FORMATLIST_CONSTRUCTOR(n) \
\
template<TINYFORMAT_ARGTYPES(n)> \
FormatListN(TINYFORMAT_VARARGS(n)) \
: FormatList(&m_formatterStore[0], n) \
{ assert(n == N); init(0, TINYFORMAT_PASSARGS(n)); } \
\
template<TINYFORMAT_ARGTYPES(n)> \
void init(int i, TINYFORMAT_VARARGS(n)) \
{ \
m_formatterStore[i] = FormatArg(v1); \
init(i+1 TINYFORMAT_PASSARGS_TAIL(n)); \
}
TINYFORMAT_FOREACH_ARGNUM(TINYFORMAT_MAKE_FORMATLIST_CONSTRUCTOR)
# undef TINYFORMAT_MAKE_FORMATLIST_CONSTRUCTOR
#endif
private:
FormatArg m_formatterStore[N];
};
// Special 0-arg version - MSVC says zero-sized C array in struct is nonstandard
template<> class FormatListN<0> : public FormatList
{
public: FormatListN() : FormatList(0, 0) {}
};
} // namespace detail
//------------------------------------------------------------------------------
// Primary API functions
#ifdef TINYFORMAT_USE_VARIADIC_TEMPLATES
/// Make type-agnostic format list from list of template arguments.
///
/// The exact return type of this function is an implementation detail and
/// shouldn't be relied upon. Instead it should be stored as a FormatListRef:
///
/// FormatListRef formatList = makeFormatList( /*...*/ );
template<typename... Args>
detail::FormatListN<sizeof...(Args)> makeFormatList(const Args&... args)
{
return detail::FormatListN<sizeof...(args)>(args...);
}
#else // C++98 version
inline detail::FormatListN<0> makeFormatList()
{
return detail::FormatListN<0>();
}
#define TINYFORMAT_MAKE_MAKEFORMATLIST(n) \
template<TINYFORMAT_ARGTYPES(n)> \
detail::FormatListN<n> makeFormatList(TINYFORMAT_VARARGS(n)) \
{ \
return detail::FormatListN<n>(TINYFORMAT_PASSARGS(n)); \
}
TINYFORMAT_FOREACH_ARGNUM(TINYFORMAT_MAKE_MAKEFORMATLIST)
#undef TINYFORMAT_MAKE_MAKEFORMATLIST
#endif
/// Format list of arguments to the stream according to the given format string.
///
/// The name vformat() is chosen for the semantic similarity to vprintf(): the
/// list of format arguments is held in a single function argument.
inline void vformat(std::ostream& out, const char* fmt, FormatListRef list)
{
detail::formatImpl(out, fmt, list.m_formatters, list.m_N);
}
#ifdef TINYFORMAT_USE_VARIADIC_TEMPLATES
/// Format list of arguments to the stream according to given format string.
template<typename... Args>
void format(std::ostream& out, const char* fmt, const Args&... args)
{
vformat(out, fmt, makeFormatList(args...));
}
/// Format list of arguments according to the given format string and return
/// the result as a string.
template<typename... Args>
std::string format(const char* fmt, const Args&... args)
{
std::ostringstream oss;
+ tfm::imbue(oss);
format(oss, fmt, args...);
return oss.str();
}
/// Format list of arguments to std::cout, according to the given format string
template<typename... Args>
void printf(const char* fmt, const Args&... args)
{
format(std::cout, fmt, args...);
}
template<typename... Args>
void printfln(const char* fmt, const Args&... args)
{
format(std::cout, fmt, args...);
std::cout << '\n';
}
#else // C++98 version
inline void format(std::ostream& out, const char* fmt)
{
vformat(out, fmt, makeFormatList());
}
inline std::string format(const char* fmt)
{
std::ostringstream oss;
+ tfm::imbue(oss);
format(oss, fmt);
return oss.str();
}
inline void printf(const char* fmt)
{
format(std::cout, fmt);
}
inline void printfln(const char* fmt)
{
format(std::cout, fmt);
std::cout << '\n';
}
#define TINYFORMAT_MAKE_FORMAT_FUNCS(n) \
\
template<TINYFORMAT_ARGTYPES(n)> \
void format(std::ostream& out, const char* fmt, TINYFORMAT_VARARGS(n)) \
{ \
vformat(out, fmt, makeFormatList(TINYFORMAT_PASSARGS(n))); \
} \
\
template<TINYFORMAT_ARGTYPES(n)> \
std::string format(const char* fmt, TINYFORMAT_VARARGS(n)) \
{ \
std::ostringstream oss; \
+ tfm::imbue(oss); \
format(oss, fmt, TINYFORMAT_PASSARGS(n)); \
return oss.str(); \
} \
\
template<TINYFORMAT_ARGTYPES(n)> \
void printf(const char* fmt, TINYFORMAT_VARARGS(n)) \
{ \
format(std::cout, fmt, TINYFORMAT_PASSARGS(n)); \
} \
\
template<TINYFORMAT_ARGTYPES(n)> \
void printfln(const char* fmt, TINYFORMAT_VARARGS(n)) \
{ \
format(std::cout, fmt, TINYFORMAT_PASSARGS(n)); \
std::cout << '\n'; \
}
TINYFORMAT_FOREACH_ARGNUM(TINYFORMAT_MAKE_FORMAT_FUNCS)
#undef TINYFORMAT_MAKE_FORMAT_FUNCS
#endif
} // namespace tinyformat
#endif // TINYFORMAT_H_INCLUDED

File Metadata

Mime Type
text/x-diff
Expires
Sat, May 16, 7:13 PM (1 d, 10 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
63285
Default Alt Text
(718 KB)

Event Timeline