From 12a9b92af0869cbf6f6e627f743a04fa638ee97e Mon Sep 17 00:00:00 2001 From: KroArtem Date: Sun, 16 Feb 2014 00:32:40 +0400 Subject: [PATCH 01/38] replace auto_ptr by unique_ptr as auto_ptr is deprecated --- src/challenges/challenge_data.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/challenges/challenge_data.cpp b/src/challenges/challenge_data.cpp index ae0c6ab9d..301140d51 100644 --- a/src/challenges/challenge_data.cpp +++ b/src/challenges/challenge_data.cpp @@ -17,6 +17,7 @@ // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. #include "challenges/challenge_data.hpp" +#include #include #include @@ -54,9 +55,9 @@ ChallengeData::ChallengeData(const std::string& filename) m_ai_superpower[d] = RaceManager::SUPERPOWER_NONE; } - // we are using auto_ptr to make sure the XML node is released when leaving + // we are using unique_ptr to make sure the XML node is released when leaving // the scope - std::auto_ptr root(new XMLNode( filename )); + std::unique_ptr root(new XMLNode( filename )); if(root.get() == NULL || root->getName()!="challenge") { From d2179e960de409419dbd3f2e5bcc617ab5119d9e Mon Sep 17 00:00:00 2001 From: KroArtem Date: Fri, 21 Feb 2014 23:09:10 +0400 Subject: [PATCH 02/38] #1156, fix quotes in CMakeLists files, see http://cmake.org/Wiki/CMake/Language_Syntax#Quoting for more info --- CMakeLists.txt | 2 +- lib/irrlicht/CMakeLists.txt | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index eb544bd66..0aa20f5f0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,7 +3,7 @@ project(SuperTuxKart) set(PROJECT_VERSION "0.8.1") cmake_minimum_required(VERSION 2.8.1) -set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${PROJECT_SOURCE_DIR}/cmake) +set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${PROJECT_SOURCE_DIR}/cmake") include(BuildTypeSTKRelease) if (NOT CMAKE_BUILD_TYPE) diff --git a/lib/irrlicht/CMakeLists.txt b/lib/irrlicht/CMakeLists.txt index 1fed7ea4b..e1200309d 100644 --- a/lib/irrlicht/CMakeLists.txt +++ b/lib/irrlicht/CMakeLists.txt @@ -1,13 +1,13 @@ # CMakeLists.txt for Irrlicht in STK -include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include/ - ${CMAKE_CURRENT_SOURCE_DIR}/source/Irrlicht/jpeglib - ${CMAKE_CURRENT_SOURCE_DIR}/source/Irrlicht/libpng - ${CMAKE_CURRENT_SOURCE_DIR}/source/Irrlicht/zlib - ${CMAKE_CURRENT_SOURCE_DIR}/source/Irrlicht/bzip2) +include_directories("${CMAKE_CURRENT_SOURCE_DIR}/include/" + "${CMAKE_CURRENT_SOURCE_DIR}/source/Irrlicht/jpeglib" + "${CMAKE_CURRENT_SOURCE_DIR}/source/Irrlicht/libpng" + "${CMAKE_CURRENT_SOURCE_DIR}/source/Irrlicht/zlib" + "${CMAKE_CURRENT_SOURCE_DIR}/source/Irrlicht/bzip2") if(APPLE) - include_directories(${CMAKE_CURRENT_SOURCE_DIR}/source/Irrlicht/MacOSX ${CMAKE_CURRENT_SOURCE_DIR}/source/Irrlicht) + include_directories("${CMAKE_CURRENT_SOURCE_DIR}/source/Irrlicht/MacOSX" "${CMAKE_CURRENT_SOURCE_DIR}/source/Irrlicht") set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -arch i386") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -arch i386 -F/Library/Frameworks") endif() From 9030291340cff5f1786267034cb2040d2088aa8e Mon Sep 17 00:00:00 2001 From: Guillaume P Date: Sat, 15 Mar 2014 10:47:36 +0100 Subject: [PATCH 03/38] Now IA don't fire a cake if the kart is driving too slow. Also correct some comments. --- src/karts/controller/skidding_ai.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/karts/controller/skidding_ai.cpp b/src/karts/controller/skidding_ai.cpp index 5db0c83c3..9eeb44211 100644 --- a/src/karts/controller/skidding_ai.cpp +++ b/src/karts/controller/skidding_ai.cpp @@ -1242,11 +1242,12 @@ void SkiddingAI::handleItems(const float dt) case PowerupManager::POWERUP_CAKE: { // Do not destroy your own shield - if(m_kart->getShieldTime() > min_bubble_time) // if the kart has a shield, do not break it by using a swatter. + if(m_kart->getShieldTime() > min_bubble_time) // if the kart has a shield, do not break it by using a cake. break; // Leave some time between shots if(m_time_since_last_shot<3.0f) break; - //TODO: do not fire if the kart is driving too slow + // Do not fire if the kart is driving too slow + if (m_kart->getSpeed() < 0.5 * m_kart->getCurrentMaxSpeed()) break; // Since cakes can be fired all around, just use a sane distance // with a bit of extra for backwards, as enemy will go towards cake @@ -1265,7 +1266,7 @@ void SkiddingAI::handleItems(const float dt) case PowerupManager::POWERUP_BOWLING: { // Do not destroy your own shield - if(m_kart->getShieldTime() > min_bubble_time) // if the kart has a shield, do not break it by using a swatter. + if(m_kart->getShieldTime() > min_bubble_time) // if the kart has a shield, do not break it by using a bowling ball. break; // Leave more time between bowling balls, since they are // slower, so it should take longer to hit something which @@ -1295,7 +1296,7 @@ void SkiddingAI::handleItems(const float dt) case PowerupManager::POWERUP_PLUNGER: { // Do not destroy your own shield - if(m_kart->getShieldTime() > min_bubble_time) // if the kart has a shield, do not break it by using a swatter. + if(m_kart->getShieldTime() > min_bubble_time) // if the kart has a shield, do not break it by using a plunger. break; // Leave more time after a plunger, since it will take some From 8a34be1ab33ae936413be5a48b9152fe3ee913e9 Mon Sep 17 00:00:00 2001 From: Guillaume P Date: Sat, 15 Mar 2014 15:08:18 +0100 Subject: [PATCH 04/38] Add a new Gold Driver achievement. --- data/achievements.xml | 7 ++++++ src/achievements/achievement_info.hpp | 13 +++++----- src/karts/controller/skidding_ai.cpp | 2 +- src/modes/world.cpp | 34 +++++++++++++++++++++++++++ src/modes/world.hpp | 1 + 5 files changed, 50 insertions(+), 7 deletions(-) diff --git a/data/achievements.xml b/data/achievements.xml index 1cfd510fa..12ecbf378 100644 --- a/data/achievements.xml +++ b/data/achievements.xml @@ -25,5 +25,12 @@ title="Marathoner" description="Make a race with 5 laps or more"> + + + + + + diff --git a/src/achievements/achievement_info.hpp b/src/achievements/achievement_info.hpp index 6b8bab458..41dce20da 100644 --- a/src/achievements/achievement_info.hpp +++ b/src/achievements/achievement_info.hpp @@ -42,12 +42,13 @@ class AchievementInfo { public: /** Some handy names for the various achievements. */ - enum { ACHIEVE_COLUMBUS = 1, - ACHIEVE_FIRST = ACHIEVE_COLUMBUS, - ACHIEVE_STRIKE = 2, - ACHIEVE_ARCH_ENEMY = 3, - ACHIEVE_MARATHONER = 4, - ACHIEVE_LAST = ACHIEVE_MARATHONER + enum { ACHIEVE_COLUMBUS = 1, + ACHIEVE_FIRST = ACHIEVE_COLUMBUS, + ACHIEVE_STRIKE = 2, + ACHIEVE_ARCH_ENEMY = 3, + ACHIEVE_MARATHONER = 4, + ACHIEVE_GOLD_DRIVER = 5, + ACHIEVE_LAST = ACHIEVE_GOLD_DRIVER }; /** Achievement check type: * ALL_AT_LEAST: All goal values must be reached (or exceeded). diff --git a/src/karts/controller/skidding_ai.cpp b/src/karts/controller/skidding_ai.cpp index 9eeb44211..b905ab2bd 100644 --- a/src/karts/controller/skidding_ai.cpp +++ b/src/karts/controller/skidding_ai.cpp @@ -1247,7 +1247,7 @@ void SkiddingAI::handleItems(const float dt) // Leave some time between shots if(m_time_since_last_shot<3.0f) break; // Do not fire if the kart is driving too slow - if (m_kart->getSpeed() < 0.5 * m_kart->getCurrentMaxSpeed()) break; + if (m_kart->getSpeed() < 0.5 * m_kart->getCurrentMaxSpeed()) break; // Since cakes can be fired all around, just use a sane distance // with a bit of extra for backwards, as enemy will go towards cake diff --git a/src/modes/world.cpp b/src/modes/world.cpp index e90b89dbb..85789af83 100644 --- a/src/modes/world.cpp +++ b/src/modes/world.cpp @@ -445,6 +445,7 @@ void World::terminateRace() &best_player); } + // Check achievements PlayerManager::increaseAchievement(AchievementInfo::ACHIEVE_COLUMBUS, getTrack()->getIdent(), 1); if (raceHasLaps()) @@ -452,6 +453,39 @@ void World::terminateRace() PlayerManager::increaseAchievement(AchievementInfo::ACHIEVE_MARATHONER, "laps", race_manager->getNumLaps()); } + + Achievement *achiev = PlayerManager::getCurrentAchievementsStatus()->getAchievement(AchievementInfo::ACHIEVE_GOLD_DRIVER); + if (achiev) + { + std::string modeName = getIdent(); // Get the race mode name + int winnerPosition = 1; + int opponents = achiev->getInfo()->getGoalValue("opponents"); // Get the required opponents number + if (modeName == IDENT_FTL) + { + winnerPosition = 2; + opponents++; + } + for(unsigned int i = 0; i < kart_amount ; i++) + { + // Retrieve the current player + StateManager::ActivePlayer* p = m_karts[i]->getController()->getPlayer(); + if (p && p->getConstProfile() == PlayerManager::get()->getCurrentPlayer()) + { + // Check if the player has won + if (m_karts[i]->getPosition() == winnerPosition && kart_amount > opponents ) + { + // Update the achievement + std::transform(modeName.begin(), modeName.end(), modeName.begin(), std::tolower); + if (achiev->getValue("opponents") <= 0) + PlayerManager::increaseAchievement(AchievementInfo::ACHIEVE_GOLD_DRIVER, + "opponents", opponents); + PlayerManager::increaseAchievement(AchievementInfo::ACHIEVE_GOLD_DRIVER, + modeName, 1); + } + } + } // for i < kart_amount + } // if (achiev) + PlayerManager::get()->getCurrentPlayer()->raceFinished(); if (m_race_gui) m_race_gui->clearAllMessages(); diff --git a/src/modes/world.hpp b/src/modes/world.hpp index d8fe1c6a6..808f39ebf 100644 --- a/src/modes/world.hpp +++ b/src/modes/world.hpp @@ -25,6 +25,7 @@ * battle, etc.) */ +#include #include #include "modes/world_status.hpp" From 5af377da07c270b14dd4988fefe9fc933603cef1 Mon Sep 17 00:00:00 2001 From: Guillaume P Date: Sat, 15 Mar 2014 15:58:17 +0100 Subject: [PATCH 05/38] Fix a build issue for Travis CI. --- src/modes/world.cpp | 2 +- src/modes/world.hpp | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/modes/world.cpp b/src/modes/world.cpp index 85789af83..53903f66f 100644 --- a/src/modes/world.cpp +++ b/src/modes/world.cpp @@ -475,7 +475,7 @@ void World::terminateRace() if (m_karts[i]->getPosition() == winnerPosition && kart_amount > opponents ) { // Update the achievement - std::transform(modeName.begin(), modeName.end(), modeName.begin(), std::tolower); + modeName = StringUtils::toLowerCase(modeName); if (achiev->getValue("opponents") <= 0) PlayerManager::increaseAchievement(AchievementInfo::ACHIEVE_GOLD_DRIVER, "opponents", opponents); diff --git a/src/modes/world.hpp b/src/modes/world.hpp index 808f39ebf..d8fe1c6a6 100644 --- a/src/modes/world.hpp +++ b/src/modes/world.hpp @@ -25,7 +25,6 @@ * battle, etc.) */ -#include #include #include "modes/world_status.hpp" From d4e90f83f2caa46b69442429340cf72260efee33 Mon Sep 17 00:00:00 2001 From: KroArtem Date: Sat, 15 Mar 2014 23:30:15 +0400 Subject: [PATCH 06/38] revert unique_ptr back to auto_ptr --- src/challenges/challenge_data.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/challenges/challenge_data.cpp b/src/challenges/challenge_data.cpp index 63cffb694..901f0ff6d 100644 --- a/src/challenges/challenge_data.cpp +++ b/src/challenges/challenge_data.cpp @@ -17,7 +17,6 @@ // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. #include "challenges/challenge_data.hpp" -#include #include #include @@ -52,9 +51,9 @@ ChallengeData::ChallengeData(const std::string& filename) m_ai_superpower[d] = RaceManager::SUPERPOWER_NONE; } - // we are using unique_ptr to make sure the XML node is released when leaving + // we are using auto_ptr to make sure the XML node is released when leaving // the scope - std::unique_ptr root(new XMLNode( filename )); + std::auto_ptr root(new XMLNode( filename )); if(root.get() == NULL || root->getName()!="challenge") { From 66c5d4a645f0e2a7306b08aab4837b98b33ec7d0 Mon Sep 17 00:00:00 2001 From: KroArtem Date: Sun, 16 Mar 2014 15:28:46 +0400 Subject: [PATCH 07/38] removed unused functions --- src/audio/sfx_manager.cpp | 10 ---------- src/guiengine/engine.cpp | 20 -------------------- src/guiengine/engine.hpp | 2 -- 3 files changed, 32 deletions(-) diff --git a/src/audio/sfx_manager.cpp b/src/audio/sfx_manager.cpp index 30eb98d5b..af00cb056 100644 --- a/src/audio/sfx_manager.cpp +++ b/src/audio/sfx_manager.cpp @@ -314,16 +314,6 @@ SFXBase* SFXManager::createSoundSource(SFXBuffer* buffer, return sfx; } // createSoundSource -//---------------------------------------------------------------------------- - -void SFXManager::dump() -{ - for(int n=0; n<(int)m_all_sfx.size(); n++) - { - Log::debug("SFXManager", "Sound %i : %s \n", n, m_all_sfx[n]->getBuffer()->getFileName().c_str()); - } -} - //---------------------------------------------------------------------------- SFXBase* SFXManager::createSoundSource(const std::string &name, const bool addToSFXList) diff --git a/src/guiengine/engine.cpp b/src/guiengine/engine.cpp index 69ce7b712..cd46e20ca 100644 --- a/src/guiengine/engine.cpp +++ b/src/guiengine/engine.cpp @@ -723,20 +723,6 @@ namespace GUIEngine std::vector gui_messages; - // ------------------------------------------------------------------------ - Screen* getScreenNamed(const char* name) - { - const int screenCount = g_loaded_screens.size(); - for (int n=0; n Date: Mon, 17 Mar 2014 08:10:21 +0100 Subject: [PATCH 08/38] Revert "Now IA don't fire a cake if the kart is driving too slow. Also correct some comments." This reverts commit 9030291340cff5f1786267034cb2040d2088aa8e. --- src/karts/controller/skidding_ai.cpp | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/karts/controller/skidding_ai.cpp b/src/karts/controller/skidding_ai.cpp index b905ab2bd..5db0c83c3 100644 --- a/src/karts/controller/skidding_ai.cpp +++ b/src/karts/controller/skidding_ai.cpp @@ -1242,12 +1242,11 @@ void SkiddingAI::handleItems(const float dt) case PowerupManager::POWERUP_CAKE: { // Do not destroy your own shield - if(m_kart->getShieldTime() > min_bubble_time) // if the kart has a shield, do not break it by using a cake. + if(m_kart->getShieldTime() > min_bubble_time) // if the kart has a shield, do not break it by using a swatter. break; // Leave some time between shots if(m_time_since_last_shot<3.0f) break; - // Do not fire if the kart is driving too slow - if (m_kart->getSpeed() < 0.5 * m_kart->getCurrentMaxSpeed()) break; + //TODO: do not fire if the kart is driving too slow // Since cakes can be fired all around, just use a sane distance // with a bit of extra for backwards, as enemy will go towards cake @@ -1266,7 +1265,7 @@ void SkiddingAI::handleItems(const float dt) case PowerupManager::POWERUP_BOWLING: { // Do not destroy your own shield - if(m_kart->getShieldTime() > min_bubble_time) // if the kart has a shield, do not break it by using a bowling ball. + if(m_kart->getShieldTime() > min_bubble_time) // if the kart has a shield, do not break it by using a swatter. break; // Leave more time between bowling balls, since they are // slower, so it should take longer to hit something which @@ -1296,7 +1295,7 @@ void SkiddingAI::handleItems(const float dt) case PowerupManager::POWERUP_PLUNGER: { // Do not destroy your own shield - if(m_kart->getShieldTime() > min_bubble_time) // if the kart has a shield, do not break it by using a plunger. + if(m_kart->getShieldTime() > min_bubble_time) // if the kart has a shield, do not break it by using a swatter. break; // Leave more time after a plunger, since it will take some From 45ab7e6907d4b40a865208c337bc6e9b4f16e8dd Mon Sep 17 00:00:00 2001 From: Guillaume P Date: Mon, 17 Mar 2014 08:21:49 +0100 Subject: [PATCH 09/38] Clean up some minor coding style issues --- src/modes/world.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/modes/world.cpp b/src/modes/world.cpp index 53903f66f..f03591c9a 100644 --- a/src/modes/world.cpp +++ b/src/modes/world.cpp @@ -457,30 +457,30 @@ void World::terminateRace() Achievement *achiev = PlayerManager::getCurrentAchievementsStatus()->getAchievement(AchievementInfo::ACHIEVE_GOLD_DRIVER); if (achiev) { - std::string modeName = getIdent(); // Get the race mode name - int winnerPosition = 1; + std::string mode_name = getIdent(); // Get the race mode name + int winner_position = 1; int opponents = achiev->getInfo()->getGoalValue("opponents"); // Get the required opponents number - if (modeName == IDENT_FTL) + if (mode_name == IDENT_FTL) { - winnerPosition = 2; + winner_position = 2; opponents++; } - for(unsigned int i = 0; i < kart_amount ; i++) + for(unsigned int i = 0; i < kart_amount; i++) { // Retrieve the current player StateManager::ActivePlayer* p = m_karts[i]->getController()->getPlayer(); if (p && p->getConstProfile() == PlayerManager::get()->getCurrentPlayer()) { // Check if the player has won - if (m_karts[i]->getPosition() == winnerPosition && kart_amount > opponents ) + if (m_karts[i]->getPosition() == winner_position && kart_amount > opponents ) { // Update the achievement - modeName = StringUtils::toLowerCase(modeName); + mode_name = StringUtils::toLowerCase(mode_name); if (achiev->getValue("opponents") <= 0) PlayerManager::increaseAchievement(AchievementInfo::ACHIEVE_GOLD_DRIVER, "opponents", opponents); PlayerManager::increaseAchievement(AchievementInfo::ACHIEVE_GOLD_DRIVER, - modeName, 1); + mode_name, 1); } } } // for i < kart_amount From de50ea46e89242968fb9729b67666f449fb2804f Mon Sep 17 00:00:00 2001 From: konstin Date: Mon, 17 Mar 2014 17:01:34 +0100 Subject: [PATCH 10/38] Allow karts to specifie a different sound effect for their engine; closes #1234 --- src/karts/kart_properties.cpp | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/karts/kart_properties.cpp b/src/karts/kart_properties.cpp index 8cd676fe1..670d10617 100644 --- a/src/karts/kart_properties.cpp +++ b/src/karts/kart_properties.cpp @@ -560,9 +560,17 @@ void KartProperties::getAllData(const XMLNode * root) else if (s == "small") m_engine_sfx_type = "engine_small"; else { - Log::warn("[KartProperties]", "Kart '%s' has invalid engine '%s'.", - m_name.c_str(), s.c_str()); - m_engine_sfx_type = "engine_small"; + if (sfx_manager->soundExist(s)) + { + m_engine_sfx_type = s; + } + else + { + Log::error("[KartProperties]", + "Kart '%s' has an invalid engine '%s'.", + m_name.c_str(), s.c_str()); + m_engine_sfx_type = "engine_small"; + } } #ifdef WILL_BE_ENABLED_ONCE_DONE_PROPERLY From 4dec9c3fc5ab25c511396703f8d1c01caa758655 Mon Sep 17 00:00:00 2001 From: KroArtem Date: Mon, 17 Mar 2014 23:56:49 +0400 Subject: [PATCH 11/38] having fun reverting changes --- src/guiengine/engine.cpp | 7 +++++++ src/guiengine/engine.hpp | 2 ++ 2 files changed, 9 insertions(+) diff --git a/src/guiengine/engine.cpp b/src/guiengine/engine.cpp index cd46e20ca..a9854516e 100644 --- a/src/guiengine/engine.cpp +++ b/src/guiengine/engine.cpp @@ -786,7 +786,14 @@ namespace GUIEngine { return Private::small_font_height; } // getSmallFontHeight + + // ------------------------------------------------------------------------ + int getLargeFontHeight() + { + return Private::large_font_height; + } // getSmallFontHeight + // ------------------------------------------------------------------------ void clear() { diff --git a/src/guiengine/engine.hpp b/src/guiengine/engine.hpp index a978f8106..5dcfce20d 100644 --- a/src/guiengine/engine.hpp +++ b/src/guiengine/engine.hpp @@ -169,6 +169,8 @@ namespace GUIEngine */ inline Skin* getSkin() { return Private::g_skin; } + Screen* getScreenNamed(const char* name); + /** \return the height of the title font in pixels */ int getTitleFontHeight(); From 89d7663112da666c76907cbf142b6c7a0c7a2f71 Mon Sep 17 00:00:00 2001 From: sudip1401 Date: Tue, 18 Mar 2014 01:39:50 +0530 Subject: [PATCH 12/38] Poweruplover achievement --- data/achievements.xml | 4 ++++ src/achievements/achievement_info.hpp | 9 +++++---- src/items/powerup.cpp | 9 ++++++++- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/data/achievements.xml b/data/achievements.xml index feb017a98..551c882cc 100644 --- a/data/achievements.xml +++ b/data/achievements.xml @@ -29,5 +29,9 @@ title="Skid-row" description="Make 5 skidding in a single race"> + + + diff --git a/src/achievements/achievement_info.hpp b/src/achievements/achievement_info.hpp index 5e52989bf..21ac3bba2 100644 --- a/src/achievements/achievement_info.hpp +++ b/src/achievements/achievement_info.hpp @@ -33,7 +33,7 @@ class Achievement; -/** This is the base class for storing the definition of an achievement, e.g. +/** This is the base class for storing the definition of an achievement, e.g. * title, description (which is common for all achievements), but also how * to achieve this achievement. * \ingroup achievements @@ -47,13 +47,14 @@ public: ACHIEVE_STRIKE = 2, ACHIEVE_ARCH_ENEMY = 3, ACHIEVE_MARATHONER = 4, - ACHIEVE_SKIDDING = 5 + ACHIEVE_SKIDDING = 5, + ACHIEVE_POWERUP_LOVER = 6 }; - /** Achievement check type: + /** Achievement check type: * ALL_AT_LEAST: All goal values must be reached (or exceeded). * ONE_AT_LEAST: At least one current value reaches or exceedes the goal. */ - enum AchievementCheckType + enum AchievementCheckType { AC_ALL_AT_LEAST, AC_ONE_AT_LEAST diff --git a/src/items/powerup.cpp b/src/items/powerup.cpp index c86cc85cd..37652d70a 100644 --- a/src/items/powerup.cpp +++ b/src/items/powerup.cpp @@ -18,6 +18,9 @@ #include "items/powerup.hpp" +#include "achievements/achievement_info.hpp" +#include "config/player_manager.hpp" + #include "audio/sfx_base.hpp" #include "audio/sfx_manager.hpp" #include "config/stk_config.hpp" @@ -170,6 +173,10 @@ void Powerup::adjustSound() */ void Powerup::use() { + // The player gets an achievement point for using a powerup + if (m_type != PowerupManager::POWERUP_NOTHING && m_owner->getController()->isPlayerController()) + PlayerManager::increaseAchievement(AchievementInfo::ACHIEVE_POWERUP_LOVER, "poweruplover"); + // Play custom kart sound when collectible is used //TODO: what about the bubble gum? if (m_type != PowerupManager::POWERUP_NOTHING && m_type != PowerupManager::POWERUP_SWATTER && @@ -237,7 +244,7 @@ void Powerup::use() m_sound_use->play(); pos.setY(hit_point.getY()-0.05f); - + ItemManager::get()->newItem(Item::ITEM_BUBBLEGUM, pos, normal, m_owner); } else // if the kart is looking forward, use the bubblegum as a shield From e9367dd1c96c0ea47e13e7049dfbbedf1ecee3a6 Mon Sep 17 00:00:00 2001 From: hiker Date: Tue, 18 Mar 2014 07:48:52 +1100 Subject: [PATCH 13/38] Fixed potential crash if RelationInfo should not exist, which I saw once while a decline was in progress), started some minor refactoring. --- .../dialogs/user_info_dialog.cpp | 26 ++++++++++++++++--- .../dialogs/user_info_dialog.hpp | 2 ++ 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/states_screens/dialogs/user_info_dialog.cpp b/src/states_screens/dialogs/user_info_dialog.cpp index bd06dcde1..b89aee79d 100644 --- a/src/states_screens/dialogs/user_info_dialog.cpp +++ b/src/states_screens/dialogs/user_info_dialog.cpp @@ -278,6 +278,24 @@ void UserInfoDialog::declineFriendRequest() } // declineFriendRequest +// ----------------------------------------------------------------------------- +/** Removes an existing friend. + */ +void UserInfoDialog::removeExistingFriend() +{ + CurrentUser::get()->requestRemoveFriend(m_profile->getID()); + +} // removeExistingFriend + +// ----------------------------------------------------------------------------- +/** Removes a pending friend request. + */ +void UserInfoDialog::removePendingFriend() +{ + CurrentUser::get()->requestCancelFriend(m_profile->getID()); + +} // removePendingFriend + // ----------------------------------------------------------------------------- GUIEngine::EventPropagation UserInfoDialog::processEvent(const std::string& eventSource) { @@ -304,10 +322,12 @@ GUIEngine::EventPropagation UserInfoDialog::processEvent(const std::string& even } else if(selection == m_remove_widget->m_properties[PROP_ID]) { - if(m_profile->getRelationInfo()->isPending()) - CurrentUser::get()->requestCancelFriend(m_profile->getID()); + if (m_profile->getRelationInfo() && + m_profile->getRelationInfo()->isPending() ) + removePendingFriend(); else - CurrentUser::get()->requestRemoveFriend(m_profile->getID()); + removeExistingFriend(); + m_processing = true; m_options_widget->setDeactivated(); return GUIEngine::EVENT_BLOCK; diff --git a/src/states_screens/dialogs/user_info_dialog.hpp b/src/states_screens/dialogs/user_info_dialog.hpp index ca6c23c78..ed5c01bd8 100644 --- a/src/states_screens/dialogs/user_info_dialog.hpp +++ b/src/states_screens/dialogs/user_info_dialog.hpp @@ -63,6 +63,8 @@ private: void sendFriendRequest(); void acceptFriendRequest(); void declineFriendRequest(); + void removeExistingFriend(); + void removePendingFriend(); public: UserInfoDialog(uint32_t showing_id, const core::stringw info = "", bool error = false, bool from_queue = false); From 72920ab0abe545bbb2e4130d0bfee80385fbc705 Mon Sep 17 00:00:00 2001 From: sudip1401 Date: Tue, 18 Mar 2014 02:20:51 +0530 Subject: [PATCH 14/38] Poweruplover achievement --- src/achievements/achievement_info.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/achievements/achievement_info.hpp b/src/achievements/achievement_info.hpp index 21ac3bba2..cb14bb90d 100644 --- a/src/achievements/achievement_info.hpp +++ b/src/achievements/achievement_info.hpp @@ -36,8 +36,8 @@ class Achievement; /** This is the base class for storing the definition of an achievement, e.g. * title, description (which is common for all achievements), but also how * to achieve this achievement. - * \ingroup achievements - */ + * \ingroup achievements + */ class AchievementInfo { public: From 3d6fd8e7921050ce8949135f77ded989f85f7c70 Mon Sep 17 00:00:00 2001 From: Marianne Gagnon Date: Mon, 17 Mar 2014 18:52:36 -0400 Subject: [PATCH 15/38] Add patch to mention firing back in tutorial --- src/tracks/track_object_presentation.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/tracks/track_object_presentation.cpp b/src/tracks/track_object_presentation.cpp index b1f8fdc66..64016359e 100644 --- a/src/tracks/track_object_presentation.cpp +++ b/src/tracks/track_object_presentation.cpp @@ -726,6 +726,16 @@ void TrackObjectPresentationActionTrigger::onTriggerItemApproached(Item* who) new TutorialMessageDialog(_("Collect gift boxes, and fire the weapon with <%s> to blow away these boxes!", fire), true); } + else if (m_action == "tutorial_backgiftboxes") + { + m_action_active = false; + InputDevice* device = input_manager->getDeviceList()->getLatestUsedDevice(); + DeviceConfig* config = device->getConfiguration(); + irr::core::stringw fire = config->getBindingAsString(PA_FIRE); + + new TutorialMessageDialog(_("Press to look behind, to fire the weapon with <%s> while pressing to to fire behind!", fire), + true); + } else if (m_action == "tutorial_nitro_collect") { m_action_active = false; From 6b8f12b1b5b4f668cbf5db505dcf5b682f146265 Mon Sep 17 00:00:00 2001 From: Nathan Osman Date: Mon, 17 Mar 2014 19:02:53 -0700 Subject: [PATCH 16/38] Check for libbluetooth on Unix platforms. --- lib/wiiuse/CMakeLists.txt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/wiiuse/CMakeLists.txt b/lib/wiiuse/CMakeLists.txt index 04e5c1e92..eabc0836e 100644 --- a/lib/wiiuse/CMakeLists.txt +++ b/lib/wiiuse/CMakeLists.txt @@ -2,6 +2,16 @@ cmake_minimum_required(VERSION 2.8.1) +# libbluetooth is required on Unix platforms +if(UNIX) + include(FindPkgConfig) + pkg_check_modules(BLUETOOTH bluez) + if(NOT BLUETOOTH_FOUND) + message(FATAL_ERROR "Bluetooth library not found. " + "Either install libbluetooth or disable wiiuse support with -DUSE_WIIUSE=0") + endif() +endif() + set(WIIUSE_SOURCES classic.c dynamics.c From 0bca25504a40ad8f269ff20f54e5d0f458001e20 Mon Sep 17 00:00:00 2001 From: sudip1401 Date: Tue, 18 Mar 2014 13:18:50 +0530 Subject: [PATCH 17/38] Poweruplover achievement --- src/items/powerup.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/items/powerup.cpp b/src/items/powerup.cpp index 37652d70a..201047dcd 100644 --- a/src/items/powerup.cpp +++ b/src/items/powerup.cpp @@ -174,8 +174,12 @@ void Powerup::adjustSound() void Powerup::use() { // The player gets an achievement point for using a powerup - if (m_type != PowerupManager::POWERUP_NOTHING && m_owner->getController()->isPlayerController()) + StateManager::ActivePlayer * player = m_owner->getController()->getPlayer(); + if (m_type != PowerupManager::POWERUP_NOTHING && + player != NULL && player->getConstProfile() == PlayerManager::get()->getCurrentPlayer()) + { PlayerManager::increaseAchievement(AchievementInfo::ACHIEVE_POWERUP_LOVER, "poweruplover"); + } // Play custom kart sound when collectible is used //TODO: what about the bubble gum? if (m_type != PowerupManager::POWERUP_NOTHING && From 2173ae828072b1eca9c8daabc43ef3aa4e2fa345 Mon Sep 17 00:00:00 2001 From: hiker Date: Tue, 18 Mar 2014 21:17:40 +1100 Subject: [PATCH 18/38] Try to work around the currently reported build problem by travis. --- src/graphics/glwrap.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/graphics/glwrap.cpp b/src/graphics/glwrap.cpp index 2b5a50f4f..b9a4f2caa 100644 --- a/src/graphics/glwrap.cpp +++ b/src/graphics/glwrap.cpp @@ -200,7 +200,7 @@ void initGL() #endif #endif #ifdef ARB_DEBUG_OUTPUT - glDebugMessageCallbackARB((GLDEBUGPROCARB)debugCallback, NULL); +// FIXME!!! glDebugMessageCallbackARB((GLDEBUGPROCARB)debugCallback, NULL); #endif } From 9042a4e0f21e5cacee9c2f3bfcb3dc5162cfc431 Mon Sep 17 00:00:00 2001 From: Marianne Gagnon Date: Tue, 18 Mar 2014 19:26:36 -0400 Subject: [PATCH 19/38] Fix bogus merge, sorry --- src/achievements/achievement_info.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/achievements/achievement_info.hpp b/src/achievements/achievement_info.hpp index 32e111ee9..4443a7c5e 100644 --- a/src/achievements/achievement_info.hpp +++ b/src/achievements/achievement_info.hpp @@ -48,7 +48,7 @@ public: ACHIEVE_ARCH_ENEMY = 3, ACHIEVE_MARATHONER = 4, ACHIEVE_SKIDDING = 5, - ACHIEVE_GOLD_DRIVER = 6 + ACHIEVE_GOLD_DRIVER = 6, ACHIEVE_POWERUP_LOVER = 7 }; /** Achievement check type: From 66d2404243dfb54270dcddd9e7765a2f6d829cdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20Kr=C3=BCger?= Date: Wed, 19 Mar 2014 00:57:50 +0100 Subject: [PATCH 20/38] travis: remove some useless options, condense irc message, skip joins in channel --- .travis.yml | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 983e7166d..f15d50e51 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,9 +6,7 @@ language: cpp compiler: - gcc - #- clang -git: - submodules: false +# - clang #branches: # only: # - master @@ -24,11 +22,10 @@ notifications: irc: channels: - "irc.freenode.org#stk" - skip_join: false - use_notice: true template: #- "[%{commit}: %{author}] %{message}" #- "%{build_url}" - - "[%{repository}#%{branch} @%{commit}] %{author}): %{message}" - - "Diff: %{compare_url}" + - "[%{repository}#%{branch} @%{commit}] %{author}): %{message} diff: %{compare_url}" - "Build: %{build_url}" + skip_join: true + use_notice: true From 433903be5d255abaa64663ae166d8c3777d33823 Mon Sep 17 00:00:00 2001 From: Vincent Lejeune Date: Wed, 19 Mar 2014 01:07:21 +0100 Subject: [PATCH 21/38] Reenable skidmark, although their color is wrong --- src/graphics/skid_marks.cpp | 3 +++ src/graphics/stkmeshscenenode.cpp | 9 +++++++++ src/graphics/stkmeshscenenode.hpp | 3 ++- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/graphics/skid_marks.cpp b/src/graphics/skid_marks.cpp index e8f0eb40a..8ee8507ed 100644 --- a/src/graphics/skid_marks.cpp +++ b/src/graphics/skid_marks.cpp @@ -25,6 +25,7 @@ #include "karts/abstract_kart.hpp" #include "karts/skidding.hpp" #include "physics/btKart.hpp" +#include "graphics/stkmeshscenenode.hpp" #include #include @@ -188,6 +189,8 @@ void SkidMarks::update(float dt, bool force_skid_marks, m_material, m_avoid_z_fighting, custom_color); new_mesh->addMeshBuffer(smq_right); scene::IMeshSceneNode *new_node = irr_driver->addMesh(new_mesh); + if (STKMeshSceneNode* stkm = dynamic_cast(new_node)) + stkm->setReloadEachFrame(); #ifdef DEBUG std::string debug_name = m_kart.getIdent()+" (skid-mark)"; new_node->setName(debug_name.c_str()); diff --git a/src/graphics/stkmeshscenenode.cpp b/src/graphics/stkmeshscenenode.cpp index bf67c4893..abb28cf35 100644 --- a/src/graphics/stkmeshscenenode.cpp +++ b/src/graphics/stkmeshscenenode.cpp @@ -32,9 +32,15 @@ STKMeshSceneNode::STKMeshSceneNode(irr::scene::IMesh* mesh, ISceneNode* parent, const irr::core::vector3df& scale) : CMeshSceneNode(mesh, parent, mgr, id, position, rotation, scale) { + reload_each_frame = false; createGLMeshes(); } +void STKMeshSceneNode::setReloadEachFrame() +{ + reload_each_frame = true; +} + void STKMeshSceneNode::createGLMeshes() { for (u32 i = 0; igetMeshBufferCount(); ++i) @@ -277,6 +283,9 @@ void STKMeshSceneNode::render() if (!Mesh || !driver) return; + if (reload_each_frame) + setMesh(Mesh); + bool isTransparentPass = SceneManager->getSceneNodeRenderPass() == scene::ESNRP_TRANSPARENT; diff --git a/src/graphics/stkmeshscenenode.hpp b/src/graphics/stkmeshscenenode.hpp index afc628335..e4ca7ead6 100644 --- a/src/graphics/stkmeshscenenode.hpp +++ b/src/graphics/stkmeshscenenode.hpp @@ -24,14 +24,15 @@ protected: void cleanGLMeshes(); void setFirstTimeMaterial(); bool isMaterialInitialized; + bool reload_each_frame; public: + void setReloadEachFrame(); STKMeshSceneNode(irr::scene::IMesh* mesh, ISceneNode* parent, irr::scene::ISceneManager* mgr, irr::s32 id, const irr::core::vector3df& position = irr::core::vector3df(0, 0, 0), const irr::core::vector3df& rotation = irr::core::vector3df(0, 0, 0), const irr::core::vector3df& scale = irr::core::vector3df(1.0f, 1.0f, 1.0f)); virtual void render(); virtual void setMesh(irr::scene::IMesh* mesh); - void MovingTexture(unsigned, unsigned); ~STKMeshSceneNode(); }; From 9a6d15a865ad1153c1e43e3f29b98737490baa0c Mon Sep 17 00:00:00 2001 From: Vincent Lejeune Date: Wed, 19 Mar 2014 01:25:44 +0100 Subject: [PATCH 22/38] Attempt to fix skidding mark's color --- data/shaders/transparent.frag | 3 ++- data/shaders/transparent.vert | 3 +++ src/graphics/shaders.cpp | 2 ++ src/graphics/shaders.hpp | 2 +- src/graphics/stkmesh.cpp | 2 +- 5 files changed, 9 insertions(+), 3 deletions(-) diff --git a/data/shaders/transparent.frag b/data/shaders/transparent.frag index 4de578afd..f6585ef9d 100644 --- a/data/shaders/transparent.frag +++ b/data/shaders/transparent.frag @@ -2,6 +2,7 @@ uniform sampler2D tex; #if __VERSION__ >= 130 in vec2 uv; +in vec4 color; out vec4 FragColor; #else varying vec2 uv; @@ -11,5 +12,5 @@ varying vec2 uv; void main() { - FragColor = texture(tex, uv); + FragColor = texture(tex, uv) * color; } diff --git a/data/shaders/transparent.vert b/data/shaders/transparent.vert index 9692fc549..e6c6b13fe 100644 --- a/data/shaders/transparent.vert +++ b/data/shaders/transparent.vert @@ -4,7 +4,9 @@ uniform mat4 TextureMatrix; #if __VERSION__ >= 130 in vec3 Position; in vec2 Texcoord; +in vec4 Color; out vec2 uv; +out vec4 color; #else attribute vec3 Position; attribute vec2 Texcoord; @@ -16,4 +18,5 @@ void main() { uv = (TextureMatrix * vec4(Texcoord, 1., 1.)).xy; gl_Position = ModelViewProjectionMatrix * vec4(Position, 1.); + color = Color; } diff --git a/src/graphics/shaders.cpp b/src/graphics/shaders.cpp index 53a1d1879..26e80c2a9 100644 --- a/src/graphics/shaders.cpp +++ b/src/graphics/shaders.cpp @@ -873,6 +873,7 @@ namespace MeshShader GLuint TransparentShader::Program; GLuint TransparentShader::attrib_position; GLuint TransparentShader::attrib_texcoord; + GLuint TransparentShader::attrib_color; GLuint TransparentShader::uniform_MVP; GLuint TransparentShader::uniform_TM; GLuint TransparentShader::uniform_tex; @@ -882,6 +883,7 @@ namespace MeshShader Program = LoadProgram(file_manager->getAsset("shaders/transparent.vert").c_str(), file_manager->getAsset("shaders/transparent.frag").c_str()); attrib_position = glGetAttribLocation(Program, "Position"); attrib_texcoord = glGetAttribLocation(Program, "Texcoord"); + attrib_color = glGetAttribLocation(Program, "Color"); uniform_MVP = glGetUniformLocation(Program, "ModelViewProjectionMatrix"); uniform_TM = glGetUniformLocation(Program, "TextureMatrix"); uniform_tex = glGetUniformLocation(Program, "tex"); diff --git a/src/graphics/shaders.hpp b/src/graphics/shaders.hpp index b9d904756..000eb1db6 100644 --- a/src/graphics/shaders.hpp +++ b/src/graphics/shaders.hpp @@ -210,7 +210,7 @@ class TransparentShader { public: static GLuint Program; - static GLuint attrib_position, attrib_texcoord; + static GLuint attrib_position, attrib_texcoord, attrib_color; static GLuint uniform_MVP, uniform_TM, uniform_tex; static void init(); diff --git a/src/graphics/stkmesh.cpp b/src/graphics/stkmesh.cpp index 1f18ad4d3..fecc72506 100644 --- a/src/graphics/stkmesh.cpp +++ b/src/graphics/stkmesh.cpp @@ -815,7 +815,7 @@ void initvaostate(GLMesh &mesh, TransparentMaterial TranspMat) break; case TM_DEFAULT: mesh.vao_first_pass = createVAO(mesh.vertex_buffer, mesh.index_buffer, - MeshShader::TransparentShader::attrib_position, MeshShader::TransparentShader::attrib_texcoord, -1, -1, -1, -1, -1, mesh.Stride); + MeshShader::TransparentShader::attrib_position, MeshShader::TransparentShader::attrib_texcoord, -1, -1, -1, -1, MeshShader::TransparentShader::attrib_color, mesh.Stride); break; } mesh.vao_glow_pass = createVAO(mesh.vertex_buffer, mesh.index_buffer, MeshShader::ColorizeShader::attrib_position, -1, -1, -1, -1, -1, -1, mesh.Stride); From 15f0445ce33ee6e531c71d007c2a56c748c5287b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20Kr=C3=BCger?= Date: Wed, 19 Mar 2014 01:38:27 +0100 Subject: [PATCH 23/38] travis: rm unneeded deps, don't build via script --- .travis.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index f15d50e51..25ef36a93 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,10 +14,13 @@ before_install: # UPDATE REPOS - sudo apt-get update -qq # INSTALL DEPENDENCIES - - sudo apt-get install autoconf automake build-essential cmake libogg-dev libvorbis-dev libopenal-dev libxxf86vm-dev libgl1-mesa-dev libglu1-mesa-dev libcurl4-openssl-dev libfribidi-dev libbluetooth-dev + - sudo apt-get install build-essential cmake libogg-dev libvorbis-dev libopenal-dev libxxf86vm-dev libgl1-mesa-dev libglu1-mesa-dev libcurl4-openssl-dev libfribidi-dev libbluetooth-dev script: # BUILD COMMANDS - - ./tools/build-linux-travis.sh + - mkdir build + - cd build + - cmake .. -DCMAKE_BUILD_TYPE=Debug + - make VERBOSE=1 -j 4 notifications: irc: channels: @@ -28,4 +31,3 @@ notifications: - "[%{repository}#%{branch} @%{commit}] %{author}): %{message} diff: %{compare_url}" - "Build: %{build_url}" skip_join: true - use_notice: true From 1e8bc6bc412e2abed4937fef5e2fe72934c8822b Mon Sep 17 00:00:00 2001 From: Vincent Lejeune Date: Wed, 19 Mar 2014 01:53:42 +0100 Subject: [PATCH 24/38] Try to avoid updating skidmark each frames. --- src/graphics/skid_marks.cpp | 4 +++- src/graphics/stkmeshscenenode.cpp | 4 ++-- src/graphics/stkmeshscenenode.hpp | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/graphics/skid_marks.cpp b/src/graphics/skid_marks.cpp index 8ee8507ed..9d519ba99 100644 --- a/src/graphics/skid_marks.cpp +++ b/src/graphics/skid_marks.cpp @@ -135,6 +135,8 @@ void SkidMarks::update(float dt, bool force_skid_marks, // (till these skid mark quads are deleted) m_left[m_current]->setHardwareMappingHint(scene::EHM_STATIC); m_right[m_current]->setHardwareMappingHint(scene::EHM_STATIC); + if (STKMeshSceneNode* stkm = dynamic_cast(m_nodes[m_current])) + stkm->setReloadEachFrame(false); return; } @@ -190,7 +192,7 @@ void SkidMarks::update(float dt, bool force_skid_marks, new_mesh->addMeshBuffer(smq_right); scene::IMeshSceneNode *new_node = irr_driver->addMesh(new_mesh); if (STKMeshSceneNode* stkm = dynamic_cast(new_node)) - stkm->setReloadEachFrame(); + stkm->setReloadEachFrame(true); #ifdef DEBUG std::string debug_name = m_kart.getIdent()+" (skid-mark)"; new_node->setName(debug_name.c_str()); diff --git a/src/graphics/stkmeshscenenode.cpp b/src/graphics/stkmeshscenenode.cpp index abb28cf35..6e7ce395e 100644 --- a/src/graphics/stkmeshscenenode.cpp +++ b/src/graphics/stkmeshscenenode.cpp @@ -36,9 +36,9 @@ STKMeshSceneNode::STKMeshSceneNode(irr::scene::IMesh* mesh, ISceneNode* parent, createGLMeshes(); } -void STKMeshSceneNode::setReloadEachFrame() +void STKMeshSceneNode::setReloadEachFrame(bool val) { - reload_each_frame = true; + reload_each_frame = val; } void STKMeshSceneNode::createGLMeshes() diff --git a/src/graphics/stkmeshscenenode.hpp b/src/graphics/stkmeshscenenode.hpp index e4ca7ead6..5a3233d2f 100644 --- a/src/graphics/stkmeshscenenode.hpp +++ b/src/graphics/stkmeshscenenode.hpp @@ -26,7 +26,7 @@ protected: bool isMaterialInitialized; bool reload_each_frame; public: - void setReloadEachFrame(); + void setReloadEachFrame(bool); STKMeshSceneNode(irr::scene::IMesh* mesh, ISceneNode* parent, irr::scene::ISceneManager* mgr, irr::s32 id, const irr::core::vector3df& position = irr::core::vector3df(0, 0, 0), const irr::core::vector3df& rotation = irr::core::vector3df(0, 0, 0), From 04f635d9cbc2c0a14553b82763b3be9fc84a8a45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20Kr=C3=BCger?= Date: Wed, 19 Mar 2014 02:33:58 +0100 Subject: [PATCH 25/38] travis: revert all changes done to notification section --- .travis.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 25ef36a93..6a0b46f4b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,9 +25,11 @@ notifications: irc: channels: - "irc.freenode.org#stk" + skip_join: false + use_notice: true template: #- "[%{commit}: %{author}] %{message}" #- "%{build_url}" - - "[%{repository}#%{branch} @%{commit}] %{author}): %{message} diff: %{compare_url}" + - "[%{repository}#%{branch} @%{commit}] %{author}): %{message}" + - "Diff: %{compare_url}" - "Build: %{build_url}" - skip_join: true From 782e280bfcadd00868232e6af56ee22d5a784907 Mon Sep 17 00:00:00 2001 From: Vincent Lejeune Date: Wed, 19 Mar 2014 18:42:48 +0100 Subject: [PATCH 26/38] Factorize PointLightShader vao and vbo. --- src/graphics/render.cpp | 58 +++----------------- src/graphics/shaders.cpp | 113 ++++++++++++++++++++++++--------------- src/graphics/shaders.hpp | 44 ++++++++++----- 3 files changed, 109 insertions(+), 106 deletions(-) diff --git a/src/graphics/render.cpp b/src/graphics/render.cpp index aba3241fa..4e33518cd 100644 --- a/src/graphics/render.cpp +++ b/src/graphics/render.cpp @@ -743,48 +743,7 @@ void IrrDriver::renderGlow(video::SOverrideMaterial &overridemat, } // ---------------------------------------------------------------------------- -#define MAXLIGHT 16 // to be adjusted in pointlight.frag too - - -static GLuint pointlightvbo = 0; -static GLuint pointlightsvao = 0; - -struct PointLightInfo -{ - float posX; - float posY; - float posZ; - float energy; - float red; - float green; - float blue; - float padding; -}; - -void createPointLightVAO() -{ - glGenVertexArrays(1, &pointlightsvao); - glBindVertexArray(pointlightsvao); - - glBindBuffer(GL_ARRAY_BUFFER, SharedObject::billboardvbo); - glEnableVertexAttribArray(MeshShader::PointLightShader::attrib_Corner); - glVertexAttribPointer(MeshShader::PointLightShader::attrib_Corner, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), 0); - - glGenBuffers(1, &pointlightvbo); - glBindBuffer(GL_ARRAY_BUFFER, pointlightvbo); - glBufferData(GL_ARRAY_BUFFER, MAXLIGHT * sizeof(PointLightInfo), 0, GL_DYNAMIC_DRAW); - - glEnableVertexAttribArray(MeshShader::PointLightShader::attrib_Position); - glVertexAttribPointer(MeshShader::PointLightShader::attrib_Position, 3, GL_FLOAT, GL_FALSE, sizeof(PointLightInfo), 0); - glEnableVertexAttribArray(MeshShader::PointLightShader::attrib_Energy); - glVertexAttribPointer(MeshShader::PointLightShader::attrib_Energy, 1, GL_FLOAT, GL_FALSE, sizeof(PointLightInfo), (GLvoid*)(3 * sizeof(float))); - glEnableVertexAttribArray(MeshShader::PointLightShader::attrib_Color); - glVertexAttribPointer(MeshShader::PointLightShader::attrib_Color, 3, GL_FLOAT, GL_FALSE, sizeof(PointLightInfo), (GLvoid*)(4 * sizeof(float))); - - glVertexAttribDivisor(MeshShader::PointLightShader::attrib_Position, 1); - glVertexAttribDivisor(MeshShader::PointLightShader::attrib_Energy, 1); - glVertexAttribDivisor(MeshShader::PointLightShader::attrib_Color, 1); -} +static LightShader::PointLightInfo PointLightsInfo[MAXLIGHT]; static void renderPointLights() { @@ -794,12 +753,14 @@ static void renderPointLights() glEnable(GL_DEPTH_TEST); glDepthMask(GL_FALSE); - glUseProgram(MeshShader::PointLightShader::Program); - glBindVertexArray(pointlightsvao); + glUseProgram(LightShader::PointLightShader::Program); + glBindVertexArray(LightShader::PointLightShader::vao); + glBindBuffer(GL_ARRAY_BUFFER, LightShader::PointLightShader::vbo); + glBufferSubData(GL_ARRAY_BUFFER, 0, MAXLIGHT * sizeof(LightShader::PointLightInfo), PointLightsInfo); setTexture(0, getTextureGLuint(irr_driver->getRTT(RTT_NORMAL_AND_DEPTH)), GL_NEAREST, GL_NEAREST); setTexture(1, getDepthTexture(irr_driver->getRTT(RTT_NORMAL_AND_DEPTH)), GL_NEAREST, GL_NEAREST); - MeshShader::PointLightShader::setUniforms(irr_driver->getViewMatrix(), irr_driver->getProjMatrix(), irr_driver->getInvProjMatrix(), core::vector2df(UserConfigParams::m_width, UserConfigParams::m_height), 200, 0, 1); + LightShader::PointLightShader::setUniforms(irr_driver->getViewMatrix(), irr_driver->getProjMatrix(), irr_driver->getInvProjMatrix(), core::vector2df(UserConfigParams::m_width, UserConfigParams::m_height), 200, 0, 1); glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, MAXLIGHT); glBindVertexArray(0); @@ -809,8 +770,6 @@ static void renderPointLights() glDisable(GL_BLEND); } -PointLightInfo PointLightsInfo[MAXLIGHT]; - void IrrDriver::renderLights(const core::aabbox3df& cambox, scene::ICameraSceneNode * const camnode, video::SOverrideMaterial &overridemat, @@ -900,11 +859,6 @@ void IrrDriver::renderLights(const core::aabbox3df& cambox, PointLightsInfo[lightnum].energy = 0; } - if (!pointlightsvao) - createPointLightVAO(); - glBindVertexArray(pointlightsvao); - glBindBuffer(GL_ARRAY_BUFFER, pointlightvbo); - glBufferSubData(GL_ARRAY_BUFFER, 0, MAXLIGHT * sizeof(PointLightInfo), PointLightsInfo); renderPointLights(); if (SkyboxCubeMap) m_post_processing->renderDiffuseEnvMap(blueSHCoeff, greenSHCoeff, redSHCoeff); diff --git a/src/graphics/shaders.cpp b/src/graphics/shaders.cpp index 26e80c2a9..b36fd7fdc 100644 --- a/src/graphics/shaders.cpp +++ b/src/graphics/shaders.cpp @@ -270,7 +270,7 @@ void Shaders::loadShaders() MeshShader::TransparentShader::init(); MeshShader::TransparentFogShader::init(); MeshShader::BillboardShader::init(); - MeshShader::PointLightShader::init(); + LightShader::PointLightShader::init(); MeshShader::DisplaceShader::init(); MeshShader::DisplaceMaskShader::init(); MeshShader::ShadowShader::init(); @@ -943,47 +943,6 @@ namespace MeshShader glUniformMatrix4fv(uniform_ipvmat, 1, GL_FALSE, ipvmat.pointer()); glUniform1i(uniform_tex, TU_tex); } - - GLuint PointLightShader::Program; - GLuint PointLightShader::attrib_Position; - GLuint PointLightShader::attrib_Color; - GLuint PointLightShader::attrib_Energy; - GLuint PointLightShader::attrib_Corner; - GLuint PointLightShader::uniform_ntex; - GLuint PointLightShader::uniform_dtex; - GLuint PointLightShader::uniform_spec; - GLuint PointLightShader::uniform_screen; - GLuint PointLightShader::uniform_invproj; - GLuint PointLightShader::uniform_VM; - GLuint PointLightShader::uniform_PM; - - void PointLightShader::init() - { - Program = LoadProgram(file_manager->getAsset("shaders/pointlight.vert").c_str(), file_manager->getAsset("shaders/pointlight.frag").c_str()); - attrib_Position = glGetAttribLocation(Program, "Position"); - attrib_Color = glGetAttribLocation(Program, "Color"); - attrib_Energy = glGetAttribLocation(Program, "Energy"); - attrib_Corner = glGetAttribLocation(Program, "Corner"); - uniform_ntex = glGetUniformLocation(Program, "ntex"); - uniform_dtex = glGetUniformLocation(Program, "dtex"); - uniform_spec = glGetUniformLocation(Program, "spec"); - uniform_invproj = glGetUniformLocation(Program, "invproj"); - uniform_screen = glGetUniformLocation(Program, "screen"); - uniform_VM = glGetUniformLocation(Program, "ViewMatrix"); - uniform_PM = glGetUniformLocation(Program, "ProjectionMatrix"); - } - - void PointLightShader::setUniforms(const core::matrix4 &ViewMatrix, const core::matrix4 &ProjMatrix, const core::matrix4 &InvProjMatrix, const core::vector2df &screen, unsigned spec, unsigned TU_ntex, unsigned TU_dtex) - { - glUniform1f(uniform_spec, 200); - glUniform2f(uniform_screen, screen.X, screen.Y); - glUniformMatrix4fv(uniform_invproj, 1, GL_FALSE, InvProjMatrix.pointer()); - glUniformMatrix4fv(uniform_VM, 1, GL_FALSE, ViewMatrix.pointer()); - glUniformMatrix4fv(uniform_PM, 1, GL_FALSE, ProjMatrix.pointer()); - - glUniform1i(uniform_ntex, TU_ntex); - glUniform1i(uniform_dtex, TU_dtex); - } GLuint BillboardShader::Program; GLuint BillboardShader::attrib_corner; @@ -1192,6 +1151,76 @@ namespace MeshShader } } +namespace LightShader +{ + + GLuint PointLightShader::Program; + GLuint PointLightShader::attrib_Position; + GLuint PointLightShader::attrib_Color; + GLuint PointLightShader::attrib_Energy; + GLuint PointLightShader::attrib_Corner; + GLuint PointLightShader::uniform_ntex; + GLuint PointLightShader::uniform_dtex; + GLuint PointLightShader::uniform_spec; + GLuint PointLightShader::uniform_screen; + GLuint PointLightShader::uniform_invproj; + GLuint PointLightShader::uniform_VM; + GLuint PointLightShader::uniform_PM; + GLuint PointLightShader::vbo; + GLuint PointLightShader::vao; + + void PointLightShader::init() + { + Program = LoadProgram(file_manager->getAsset("shaders/pointlight.vert").c_str(), file_manager->getAsset("shaders/pointlight.frag").c_str()); + attrib_Position = glGetAttribLocation(Program, "Position"); + attrib_Color = glGetAttribLocation(Program, "Color"); + attrib_Energy = glGetAttribLocation(Program, "Energy"); + attrib_Corner = glGetAttribLocation(Program, "Corner"); + uniform_ntex = glGetUniformLocation(Program, "ntex"); + uniform_dtex = glGetUniformLocation(Program, "dtex"); + uniform_spec = glGetUniformLocation(Program, "spec"); + uniform_invproj = glGetUniformLocation(Program, "invproj"); + uniform_screen = glGetUniformLocation(Program, "screen"); + uniform_VM = glGetUniformLocation(Program, "ViewMatrix"); + uniform_PM = glGetUniformLocation(Program, "ProjectionMatrix"); + + glGenVertexArrays(1, &vao); + glBindVertexArray(vao); + + glBindBuffer(GL_ARRAY_BUFFER, SharedObject::billboardvbo); + glEnableVertexAttribArray(attrib_Corner); + glVertexAttribPointer(attrib_Corner, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), 0); + + glGenBuffers(1, &vbo); + glBindBuffer(GL_ARRAY_BUFFER, vbo); + glBufferData(GL_ARRAY_BUFFER, MAXLIGHT * sizeof(PointLightInfo), 0, GL_DYNAMIC_DRAW); + + glEnableVertexAttribArray(attrib_Position); + glVertexAttribPointer(attrib_Position, 3, GL_FLOAT, GL_FALSE, sizeof(PointLightInfo), 0); + glEnableVertexAttribArray(attrib_Energy); + glVertexAttribPointer(attrib_Energy, 1, GL_FLOAT, GL_FALSE, sizeof(PointLightInfo), (GLvoid*)(3 * sizeof(float))); + glEnableVertexAttribArray(attrib_Color); + glVertexAttribPointer(attrib_Color, 3, GL_FLOAT, GL_FALSE, sizeof(PointLightInfo), (GLvoid*)(4 * sizeof(float))); + + glVertexAttribDivisor(attrib_Position, 1); + glVertexAttribDivisor(attrib_Energy, 1); + glVertexAttribDivisor(attrib_Color, 1); + } + + void PointLightShader::setUniforms(const core::matrix4 &ViewMatrix, const core::matrix4 &ProjMatrix, const core::matrix4 &InvProjMatrix, const core::vector2df &screen, unsigned spec, unsigned TU_ntex, unsigned TU_dtex) + { + glUniform1f(uniform_spec, 200); + glUniform2f(uniform_screen, screen.X, screen.Y); + glUniformMatrix4fv(uniform_invproj, 1, GL_FALSE, InvProjMatrix.pointer()); + glUniformMatrix4fv(uniform_VM, 1, GL_FALSE, ViewMatrix.pointer()); + glUniformMatrix4fv(uniform_PM, 1, GL_FALSE, ProjMatrix.pointer()); + + glUniform1i(uniform_ntex, TU_ntex); + glUniform1i(uniform_dtex, TU_dtex); + } + +} + namespace ParticleShader { diff --git a/src/graphics/shaders.hpp b/src/graphics/shaders.hpp index 000eb1db6..5cc517c8b 100644 --- a/src/graphics/shaders.hpp +++ b/src/graphics/shaders.hpp @@ -228,18 +228,6 @@ public: static void setUniforms(const core::matrix4 &ModelViewProjectionMatrix, const core::matrix4 &TextureMatrix, const core::matrix4 &ipvmat, float fogmax, float startH, float endH, float start, float end, const core::vector3df &col, const core::vector3df &campos, unsigned TU_tex); }; -class PointLightShader -{ -public: - static GLuint Program; - static GLuint attrib_Position, attrib_Energy, attrib_Color; - static GLuint attrib_Corner; - static GLuint uniform_ntex, uniform_dtex, uniform_spec, uniform_screen, uniform_invproj, uniform_VM, uniform_PM; - - static void init(); - static void setUniforms(const core::matrix4 &ViewMatrix, const core::matrix4 &ProjMatrix, const core::matrix4 &InvProjMatrix, const core::vector2df &screen, unsigned spec, unsigned TU_ntex, unsigned TU_dtex); -}; - class BillboardShader { public: @@ -331,6 +319,38 @@ public: } +#define MAXLIGHT 16 + +namespace LightShader +{ + struct PointLightInfo + { + float posX; + float posY; + float posZ; + float energy; + float red; + float green; + float blue; + float padding; + }; + + + class PointLightShader + { + public: + static GLuint Program; + static GLuint attrib_Position, attrib_Energy, attrib_Color; + static GLuint attrib_Corner; + static GLuint uniform_ntex, uniform_dtex, uniform_spec, uniform_screen, uniform_invproj, uniform_VM, uniform_PM; + static GLuint vbo; + static GLuint vao; + + static void init(); + static void setUniforms(const core::matrix4 &ViewMatrix, const core::matrix4 &ProjMatrix, const core::matrix4 &InvProjMatrix, const core::vector2df &screen, unsigned spec, unsigned TU_ntex, unsigned TU_dtex); + }; +} + namespace ParticleShader { From 4128371b7fd44b5a664a3c5c118321b0af0ef8e0 Mon Sep 17 00:00:00 2001 From: Vincent Lejeune Date: Wed, 19 Mar 2014 19:02:29 +0100 Subject: [PATCH 27/38] Do not switch program if it's not used. --- src/graphics/stkanimatedmesh.cpp | 55 +++++++++++------- src/graphics/stkmeshscenenode.cpp | 96 ++++++++++++++++++------------- 2 files changed, 90 insertions(+), 61 deletions(-) diff --git a/src/graphics/stkanimatedmesh.cpp b/src/graphics/stkanimatedmesh.cpp index 8bb937dd1..70f206d16 100644 --- a/src/graphics/stkanimatedmesh.cpp +++ b/src/graphics/stkanimatedmesh.cpp @@ -171,11 +171,13 @@ void STKAnimatedMesh::render() computeMVP(ModelViewProjectionMatrix); computeTIMV(TransposeInverseModelView); - glUseProgram(MeshShader::ObjectPass1Shader::Program); + if (!GeometricMesh[FPSM_DEFAULT].empty()) + glUseProgram(MeshShader::ObjectPass1Shader::Program); for (unsigned i = 0; i < GeometricMesh[FPSM_DEFAULT].size(); i++) drawSolidPass1(*GeometricMesh[FPSM_DEFAULT][i], FPSM_DEFAULT); - glUseProgram(MeshShader::ObjectRefPass1Shader::Program); + if (!GeometricMesh[FPSM_ALPHA_REF_TEXTURE].empty()) + glUseProgram(MeshShader::ObjectRefPass1Shader::Program); for (unsigned i = 0; i < GeometricMesh[FPSM_ALPHA_REF_TEXTURE].size(); i++) drawSolidPass1(*GeometricMesh[FPSM_ALPHA_REF_TEXTURE][i], FPSM_ALPHA_REF_TEXTURE); @@ -184,36 +186,43 @@ void STKAnimatedMesh::render() if (irr_driver->getPhase() == SOLID_LIT_PASS) { - glUseProgram(MeshShader::ObjectPass2Shader::Program); - for (unsigned i = 0; i < ShadedMesh[SM_DEFAULT].size(); i++) - drawSolidPass2(*ShadedMesh[SM_DEFAULT][i], SM_DEFAULT); + if (!ShadedMesh[SM_DEFAULT].empty()) + glUseProgram(MeshShader::ObjectPass2Shader::Program); + for (unsigned i = 0; i < ShadedMesh[SM_DEFAULT].size(); i++) + drawSolidPass2(*ShadedMesh[SM_DEFAULT][i], SM_DEFAULT); - glUseProgram(MeshShader::ObjectRefPass2Shader::Program); - for (unsigned i = 0; i < ShadedMesh[SM_ALPHA_REF_TEXTURE].size(); i++) - drawSolidPass2(*ShadedMesh[SM_ALPHA_REF_TEXTURE][i], SM_ALPHA_REF_TEXTURE); + if (!ShadedMesh[SM_ALPHA_REF_TEXTURE].empty()) + glUseProgram(MeshShader::ObjectRefPass2Shader::Program); + for (unsigned i = 0; i < ShadedMesh[SM_ALPHA_REF_TEXTURE].size(); i++) + drawSolidPass2(*ShadedMesh[SM_ALPHA_REF_TEXTURE][i], SM_ALPHA_REF_TEXTURE); - glUseProgram(MeshShader::ObjectRimLimitShader::Program); - for (unsigned i = 0; i < ShadedMesh[SM_RIMLIT].size(); i++) - drawSolidPass2(*ShadedMesh[SM_RIMLIT][i], SM_RIMLIT); + if (!ShadedMesh[SM_RIMLIT].empty()) + glUseProgram(MeshShader::ObjectRimLimitShader::Program); + for (unsigned i = 0; i < ShadedMesh[SM_RIMLIT].size(); i++) + drawSolidPass2(*ShadedMesh[SM_RIMLIT][i], SM_RIMLIT); - glUseProgram(MeshShader::ObjectUnlitShader::Program); - for (unsigned i = 0; i < ShadedMesh[SM_UNLIT].size(); i++) - drawSolidPass2(*ShadedMesh[SM_UNLIT][i], SM_UNLIT); + if (!ShadedMesh[SM_UNLIT].empty()) + glUseProgram(MeshShader::ObjectUnlitShader::Program); + for (unsigned i = 0; i < ShadedMesh[SM_UNLIT].size(); i++) + drawSolidPass2(*ShadedMesh[SM_UNLIT][i], SM_UNLIT); - glUseProgram(MeshShader::DetailledObjectPass2Shader::Program); - for (unsigned i = 0; i < ShadedMesh[SM_DETAILS].size(); i++) - drawSolidPass2(*ShadedMesh[SM_DETAILS][i], SM_DETAILS); + if (!ShadedMesh[SM_DETAILS].empty()) + glUseProgram(MeshShader::DetailledObjectPass2Shader::Program); + for (unsigned i = 0; i < ShadedMesh[SM_DETAILS].size(); i++) + drawSolidPass2(*ShadedMesh[SM_DETAILS][i], SM_DETAILS); - return; + return; } if (irr_driver->getPhase() == SHADOW_PASS) { - glUseProgram(MeshShader::ShadowShader::Program); + if (!GeometricMesh[FPSM_DEFAULT].empty()) + glUseProgram(MeshShader::ShadowShader::Program); for (unsigned i = 0; i < GeometricMesh[FPSM_DEFAULT].size(); i++) drawShadow(*GeometricMesh[FPSM_DEFAULT][i]); - glUseProgram(MeshShader::RefShadowShader::Program); + if (!GeometricMesh[FPSM_ALPHA_REF_TEXTURE].empty()) + glUseProgram(MeshShader::RefShadowShader::Program); for (unsigned i = 0; i < GeometricMesh[FPSM_ALPHA_REF_TEXTURE].size(); i++) drawShadowRef(*GeometricMesh[FPSM_ALPHA_REF_TEXTURE][i]); return; @@ -223,11 +232,13 @@ void STKAnimatedMesh::render() { computeMVP(ModelViewProjectionMatrix); - glUseProgram(MeshShader::BubbleShader::Program); + if (!TransparentMesh[TM_BUBBLE].empty()) + glUseProgram(MeshShader::BubbleShader::Program); for (unsigned i = 0; i < TransparentMesh[TM_BUBBLE].size(); i++) drawBubble(*TransparentMesh[TM_BUBBLE][i], ModelViewProjectionMatrix); - glUseProgram(MeshShader::TransparentShader::Program); + if (!TransparentMesh[TM_DEFAULT].empty()) + glUseProgram(MeshShader::TransparentShader::Program); for (unsigned i = 0; i < TransparentMesh[TM_DEFAULT].size(); i++) drawTransparentObject(*TransparentMesh[TM_DEFAULT][i], ModelViewProjectionMatrix, (*TransparentMesh[TM_DEFAULT][i]).TextureMatrix); return; diff --git a/src/graphics/stkmeshscenenode.cpp b/src/graphics/stkmeshscenenode.cpp index 6e7ce395e..6365ade59 100644 --- a/src/graphics/stkmeshscenenode.cpp +++ b/src/graphics/stkmeshscenenode.cpp @@ -309,19 +309,23 @@ void STKMeshSceneNode::render() computeMVP(ModelViewProjectionMatrix); computeTIMV(TransposeInverseModelView); - glUseProgram(MeshShader::ObjectPass1Shader::Program); + if (!GeometricMesh[FPSM_DEFAULT].empty()) + glUseProgram(MeshShader::ObjectPass1Shader::Program); for (unsigned i = 0; i < GeometricMesh[FPSM_DEFAULT].size(); i++) drawSolidPass1(*GeometricMesh[FPSM_DEFAULT][i], FPSM_DEFAULT); - glUseProgram(MeshShader::ObjectRefPass1Shader::Program); + if (!GeometricMesh[FPSM_ALPHA_REF_TEXTURE].empty()) + glUseProgram(MeshShader::ObjectRefPass1Shader::Program); for (unsigned i = 0; i < GeometricMesh[FPSM_ALPHA_REF_TEXTURE].size(); i++) drawSolidPass1(*GeometricMesh[FPSM_ALPHA_REF_TEXTURE][i], FPSM_ALPHA_REF_TEXTURE); - glUseProgram(MeshShader::NormalMapShader::Program); + if (!GeometricMesh[FPSM_NORMAL_MAP].empty()) + glUseProgram(MeshShader::NormalMapShader::Program); for (unsigned i = 0; i < GeometricMesh[FPSM_NORMAL_MAP].size(); i++) drawSolidPass1(*GeometricMesh[FPSM_NORMAL_MAP][i], FPSM_NORMAL_MAP); - glUseProgram(MeshShader::GrassPass1Shader::Program); + if (!GeometricMesh[FPSM_GRASS].empty()) + glUseProgram(MeshShader::GrassPass1Shader::Program); for (unsigned i = 0; i < GeometricMesh[FPSM_GRASS].size(); i++) drawSolidPass1(*GeometricMesh[FPSM_GRASS][i], FPSM_GRASS); @@ -330,56 +334,68 @@ void STKMeshSceneNode::render() if (irr_driver->getPhase() == SOLID_LIT_PASS) { - glUseProgram(MeshShader::ObjectPass2Shader::Program); - for (unsigned i = 0; i < ShadedMesh[SM_DEFAULT].size(); i++) - drawSolidPass2(*ShadedMesh[SM_DEFAULT][i], SM_DEFAULT); + if (!ShadedMesh[SM_DEFAULT].empty()) + glUseProgram(MeshShader::ObjectPass2Shader::Program); + for (unsigned i = 0; i < ShadedMesh[SM_DEFAULT].size(); i++) + drawSolidPass2(*ShadedMesh[SM_DEFAULT][i], SM_DEFAULT); - glUseProgram(MeshShader::ObjectRefPass2Shader::Program); - for (unsigned i = 0; i < ShadedMesh[SM_ALPHA_REF_TEXTURE].size(); i++) - drawSolidPass2(*ShadedMesh[SM_ALPHA_REF_TEXTURE][i], SM_ALPHA_REF_TEXTURE); + if (!ShadedMesh[SM_ALPHA_REF_TEXTURE].empty()) + glUseProgram(MeshShader::ObjectRefPass2Shader::Program); + for (unsigned i = 0; i < ShadedMesh[SM_ALPHA_REF_TEXTURE].size(); i++) + drawSolidPass2(*ShadedMesh[SM_ALPHA_REF_TEXTURE][i], SM_ALPHA_REF_TEXTURE); - glUseProgram(MeshShader::ObjectRimLimitShader::Program); - for (unsigned i = 0; i < ShadedMesh[SM_RIMLIT].size(); i++) - drawSolidPass2(*ShadedMesh[SM_RIMLIT][i], SM_RIMLIT); + if (!ShadedMesh[SM_RIMLIT].empty()) + glUseProgram(MeshShader::ObjectRimLimitShader::Program); + for (unsigned i = 0; i < ShadedMesh[SM_RIMLIT].size(); i++) + drawSolidPass2(*ShadedMesh[SM_RIMLIT][i], SM_RIMLIT); - glUseProgram(MeshShader::SphereMapShader::Program); - for (unsigned i = 0; i < ShadedMesh[SM_SPHEREMAP].size(); i++) - drawSolidPass2(*ShadedMesh[SM_SPHEREMAP][i], SM_SPHEREMAP); + if (!ShadedMesh[SM_SPHEREMAP].empty()) + glUseProgram(MeshShader::SphereMapShader::Program); + for (unsigned i = 0; i < ShadedMesh[SM_SPHEREMAP].size(); i++) + drawSolidPass2(*ShadedMesh[SM_SPHEREMAP][i], SM_SPHEREMAP); - glUseProgram(MeshShader::SplattingShader::Program); - for (unsigned i = 0; i < ShadedMesh[SM_SPLATTING].size(); i++) - drawSolidPass2(*ShadedMesh[SM_SPLATTING][i], SM_SPLATTING); + if (!ShadedMesh[SM_SPLATTING].empty()) + glUseProgram(MeshShader::SplattingShader::Program); + for (unsigned i = 0; i < ShadedMesh[SM_SPLATTING].size(); i++) + drawSolidPass2(*ShadedMesh[SM_SPLATTING][i], SM_SPLATTING); - glUseProgram(MeshShader::GrassPass2Shader::Program); - for (unsigned i = 0; i < ShadedMesh[SM_GRASS].size(); i++) - drawSolidPass2(*ShadedMesh[SM_GRASS][i], SM_GRASS); + if (!ShadedMesh[SM_GRASS].empty()) + glUseProgram(MeshShader::GrassPass2Shader::Program); + for (unsigned i = 0; i < ShadedMesh[SM_GRASS].size(); i++) + drawSolidPass2(*ShadedMesh[SM_GRASS][i], SM_GRASS); - glUseProgram(MeshShader::ObjectUnlitShader::Program); - for (unsigned i = 0; i < ShadedMesh[SM_UNLIT].size(); i++) - drawSolidPass2(*ShadedMesh[SM_UNLIT][i], SM_UNLIT); + if (!ShadedMesh[SM_UNLIT].empty()) + glUseProgram(MeshShader::ObjectUnlitShader::Program); + for (unsigned i = 0; i < ShadedMesh[SM_UNLIT].size(); i++) + drawSolidPass2(*ShadedMesh[SM_UNLIT][i], SM_UNLIT); - glUseProgram(MeshShader::CausticsShader::Program); - for (unsigned i = 0; i < ShadedMesh[SM_CAUSTICS].size(); i++) - drawSolidPass2(*ShadedMesh[SM_CAUSTICS][i], SM_CAUSTICS); + if (!ShadedMesh[SM_CAUSTICS].empty()) + glUseProgram(MeshShader::CausticsShader::Program); + for (unsigned i = 0; i < ShadedMesh[SM_CAUSTICS].size(); i++) + drawSolidPass2(*ShadedMesh[SM_CAUSTICS][i], SM_CAUSTICS); - glUseProgram(MeshShader::DetailledObjectPass2Shader::Program); - for (unsigned i = 0; i < ShadedMesh[SM_DETAILS].size(); i++) - drawSolidPass2(*ShadedMesh[SM_DETAILS][i], SM_DETAILS); + if (!ShadedMesh[SM_DETAILS].empty()) + glUseProgram(MeshShader::DetailledObjectPass2Shader::Program); + for (unsigned i = 0; i < ShadedMesh[SM_DETAILS].size(); i++) + drawSolidPass2(*ShadedMesh[SM_DETAILS][i], SM_DETAILS); - glUseProgram(MeshShader::UntexturedObjectShader::Program); - for (unsigned i = 0; i < ShadedMesh[SM_UNTEXTURED].size(); i++) - drawSolidPass2(*ShadedMesh[SM_UNTEXTURED][i], SM_UNTEXTURED); + if (!ShadedMesh[SM_UNTEXTURED].empty()) + glUseProgram(MeshShader::UntexturedObjectShader::Program); + for (unsigned i = 0; i < ShadedMesh[SM_UNTEXTURED].size(); i++) + drawSolidPass2(*ShadedMesh[SM_UNTEXTURED][i], SM_UNTEXTURED); - return; + return; } if (irr_driver->getPhase() == SHADOW_PASS) { - glUseProgram(MeshShader::ShadowShader::Program); + if (!GeometricMesh[FPSM_DEFAULT].empty()) + glUseProgram(MeshShader::ShadowShader::Program); for (unsigned i = 0; i < GeometricMesh[FPSM_DEFAULT].size(); i++) drawShadow(*GeometricMesh[FPSM_DEFAULT][i]); - glUseProgram(MeshShader::RefShadowShader::Program); + if (!GeometricMesh[FPSM_ALPHA_REF_TEXTURE].empty()) + glUseProgram(MeshShader::RefShadowShader::Program); for (unsigned i = 0; i < GeometricMesh[FPSM_ALPHA_REF_TEXTURE].size(); i++) drawShadowRef(*GeometricMesh[FPSM_ALPHA_REF_TEXTURE][i]); return; @@ -401,11 +417,13 @@ void STKMeshSceneNode::render() { computeMVP(ModelViewProjectionMatrix); - glUseProgram(MeshShader::BubbleShader::Program); + if (!TransparentMesh[TM_BUBBLE].empty()) + glUseProgram(MeshShader::BubbleShader::Program); for (unsigned i = 0; i < TransparentMesh[TM_BUBBLE].size(); i++) drawBubble(*TransparentMesh[TM_BUBBLE][i], ModelViewProjectionMatrix); - glUseProgram(MeshShader::TransparentShader::Program); + if (!TransparentMesh[TM_DEFAULT].empty()) + glUseProgram(MeshShader::TransparentShader::Program); for (unsigned i = 0; i < TransparentMesh[TM_DEFAULT].size(); i++) drawTransparentObject(*TransparentMesh[TM_DEFAULT][i], ModelViewProjectionMatrix, (*TransparentMesh[TM_DEFAULT][i]).TextureMatrix); return; From 822081deb48081059f5d7d1d78f227acadf921b1 Mon Sep 17 00:00:00 2001 From: vlj Date: Wed, 19 Mar 2014 23:00:45 +0100 Subject: [PATCH 28/38] Only reupload buffer for skidding mark --- src/graphics/stkmeshscenenode.cpp | 37 ++++++++++++++++++++++++++++++- src/graphics/stkmeshscenenode.hpp | 1 + 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/graphics/stkmeshscenenode.cpp b/src/graphics/stkmeshscenenode.cpp index 6365ade59..82f26df70 100644 --- a/src/graphics/stkmeshscenenode.cpp +++ b/src/graphics/stkmeshscenenode.cpp @@ -276,6 +276,41 @@ void STKMeshSceneNode::drawSolidPass2(const GLMesh &mesh, ShadedMaterial type) } } +void STKMeshSceneNode::updatevbo() +{ + for (unsigned i = 0; i < Mesh->getMeshBufferCount(); ++i) + { + scene::IMeshBuffer* mb = Mesh->getMeshBuffer(i); + if (!mb) + continue; + GLMesh &mesh = GLmeshes[i]; + glBindVertexArray(0); + + glBindBuffer(GL_ARRAY_BUFFER, mesh.vertex_buffer); + const void* vertices = mb->getVertices(); + const u32 vertexCount = mb->getVertexCount(); + const c8* vbuf = static_cast(vertices); + glBufferData(GL_ARRAY_BUFFER, vertexCount * mesh.Stride, vbuf, GL_STATIC_DRAW); + + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mesh.index_buffer); + const void* indices = mb->getIndices(); + mesh.IndexCount = mb->getIndexCount(); + GLenum indexSize; + switch (mb->getIndexType()) + { + case irr::video::EIT_16BIT: + indexSize = sizeof(u16); + break; + case irr::video::EIT_32BIT: + indexSize = sizeof(u32); + break; + default: + assert(0 && "Wrong index size"); + } + glBufferData(GL_ELEMENT_ARRAY_BUFFER, mesh.IndexCount * indexSize, indices, GL_STATIC_DRAW); + } +} + void STKMeshSceneNode::render() { irr::video::IVideoDriver* driver = irr_driver->getVideoDriver(); @@ -284,7 +319,7 @@ void STKMeshSceneNode::render() return; if (reload_each_frame) - setMesh(Mesh); + updatevbo(); bool isTransparentPass = SceneManager->getSceneNodeRenderPass() == scene::ESNRP_TRANSPARENT; diff --git a/src/graphics/stkmeshscenenode.hpp b/src/graphics/stkmeshscenenode.hpp index 5a3233d2f..fffd04bcf 100644 --- a/src/graphics/stkmeshscenenode.hpp +++ b/src/graphics/stkmeshscenenode.hpp @@ -23,6 +23,7 @@ protected: void createGLMeshes(); void cleanGLMeshes(); void setFirstTimeMaterial(); + void updatevbo(); bool isMaterialInitialized; bool reload_each_frame; public: From a3eee305cae211c535c8c55fe25219e0802786f3 Mon Sep 17 00:00:00 2001 From: vlj Date: Wed, 19 Mar 2014 23:07:40 +0100 Subject: [PATCH 29/38] Remove the max distance for lights --- src/graphics/render.cpp | 25 +++++++------------------ 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/src/graphics/render.cpp b/src/graphics/render.cpp index 4e33518cd..b0673f61d 100644 --- a/src/graphics/render.cpp +++ b/src/graphics/render.cpp @@ -743,9 +743,11 @@ void IrrDriver::renderGlow(video::SOverrideMaterial &overridemat, } // ---------------------------------------------------------------------------- +#define MAX2(a, b) ((a) > (b) ? (a) : (b)) +#define MIN2(a, b) ((a) > (b) ? (b) : (a)) static LightShader::PointLightInfo PointLightsInfo[MAXLIGHT]; -static void renderPointLights() +static void renderPointLights(unsigned count) { glEnable(GL_BLEND); glBlendEquation(GL_FUNC_ADD); @@ -756,18 +758,13 @@ static void renderPointLights() glUseProgram(LightShader::PointLightShader::Program); glBindVertexArray(LightShader::PointLightShader::vao); glBindBuffer(GL_ARRAY_BUFFER, LightShader::PointLightShader::vbo); - glBufferSubData(GL_ARRAY_BUFFER, 0, MAXLIGHT * sizeof(LightShader::PointLightInfo), PointLightsInfo); + glBufferSubData(GL_ARRAY_BUFFER, 0, count * sizeof(LightShader::PointLightInfo), PointLightsInfo); setTexture(0, getTextureGLuint(irr_driver->getRTT(RTT_NORMAL_AND_DEPTH)), GL_NEAREST, GL_NEAREST); setTexture(1, getDepthTexture(irr_driver->getRTT(RTT_NORMAL_AND_DEPTH)), GL_NEAREST, GL_NEAREST); LightShader::PointLightShader::setUniforms(irr_driver->getViewMatrix(), irr_driver->getProjMatrix(), irr_driver->getInvProjMatrix(), core::vector2df(UserConfigParams::m_width, UserConfigParams::m_height), 200, 0, 1); - glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, MAXLIGHT); - glBindVertexArray(0); - glBindBuffer(GL_ARRAY_BUFFER, 0); - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); - glEnable(GL_DEPTH_TEST); - glDisable(GL_BLEND); + glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, count); } void IrrDriver::renderLights(const core::aabbox3df& cambox, @@ -807,7 +804,7 @@ void IrrDriver::renderLights(const core::aabbox3df& cambox, const core::vector3df &lightpos = (m_lights[i]->getAbsolutePosition() - campos); unsigned idx = (unsigned)(lightpos.getLength() / 10); if (idx > 14) - continue; + idx = 14; BucketedLN[idx].push_back(m_lights[i]); } @@ -854,12 +851,7 @@ void IrrDriver::renderLights(const core::aabbox3df& cambox, lightnum++; - // Fill lights - for (; lightnum < MAXLIGHT; lightnum++) { - PointLightsInfo[lightnum].energy = 0; - } - - renderPointLights(); + renderPointLights(MIN2(lightnum, MAXLIGHT)); if (SkyboxCubeMap) m_post_processing->renderDiffuseEnvMap(blueSHCoeff, greenSHCoeff, redSHCoeff); // Handle SSAO @@ -946,9 +938,6 @@ static void createcubevao() glBufferData(GL_ELEMENT_ARRAY_BUFFER, 6 * 6 * sizeof(int), indices, GL_STATIC_DRAW); } -#define MAX2(a, b) ((a) > (b) ? (a) : (b)) -#define MIN2(a, b) ((a) > (b) ? (b) : (a)) - static void getXYZ(GLenum face, float i, float j, float &x, float &y, float &z) { switch (face) From 8616ce546a2dbc6daae7c5575d7929218819c3dd Mon Sep 17 00:00:00 2001 From: "cosmin.crecana" Date: Thu, 20 Mar 2014 15:30:11 +0200 Subject: [PATCH 30/38] achievement_unstoppable --- data/achievements.xml | 4 ++++ src/achievements/achievement_info.hpp | 3 ++- src/modes/world.cpp | 27 ++++++++++++++++++++++++++- 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/data/achievements.xml b/data/achievements.xml index 5eafd3053..7ca86134c 100644 --- a/data/achievements.xml +++ b/data/achievements.xml @@ -40,5 +40,9 @@ title="Powerup Love" description="Use 10 or more powerups in a race"> + + + diff --git a/src/achievements/achievement_info.hpp b/src/achievements/achievement_info.hpp index 4443a7c5e..637112a5c 100644 --- a/src/achievements/achievement_info.hpp +++ b/src/achievements/achievement_info.hpp @@ -49,7 +49,8 @@ public: ACHIEVE_MARATHONER = 4, ACHIEVE_SKIDDING = 5, ACHIEVE_GOLD_DRIVER = 6, - ACHIEVE_POWERUP_LOVER = 7 + ACHIEVE_POWERUP_LOVER = 7, + ACHIEVE_UNSTOPPABLE = 8 }; /** Achievement check type: * ALL_AT_LEAST: All goal values must be reached (or exceeded). diff --git a/src/modes/world.cpp b/src/modes/world.cpp index f03591c9a..d6db91624 100644 --- a/src/modes/world.cpp +++ b/src/modes/world.cpp @@ -485,7 +485,32 @@ void World::terminateRace() } } // for i < kart_amount } // if (achiev) - + + Achievement *win = PlayerManager::getCurrentAchievementsStatus()->getAchievement(AchievementInfo::ACHIEVE_UNSTOPPABLE); + //if achivement has been unlocked + if (win->getValue("wins") < 5 ) + { + for(unsigned int i = 0; i < kart_amount; i++) + { + // Retrieve the current player + StateManager::ActivePlayer* p = m_karts[i]->getController()->getPlayer(); + if (p && p->getConstProfile() == PlayerManager::get()->getCurrentPlayer()) + { + // Check if the player has won + if (m_karts[i]->getPosition() == 1 ) + { + // Increase number of consecutive wins + PlayerManager::increaseAchievement(AchievementInfo::ACHIEVE_UNSTOPPABLE, + "wins", 1); + } + else + { + //Set number of consecutive wins to 0 + win->reset(); + } + } + } + } PlayerManager::get()->getCurrentPlayer()->raceFinished(); if (m_race_gui) m_race_gui->clearAllMessages(); From b3f59437b1d13e355f0a1908615805da66304b37 Mon Sep 17 00:00:00 2001 From: vlj Date: Thu, 20 Mar 2014 18:06:59 +0100 Subject: [PATCH 31/38] Fix rubber band effect for plunger. --- src/graphics/stkmeshscenenode.cpp | 15 +++++++++++++++ src/items/rubber_band.cpp | 3 +++ 2 files changed, 18 insertions(+) diff --git a/src/graphics/stkmeshscenenode.cpp b/src/graphics/stkmeshscenenode.cpp index 82f26df70..34dde76c3 100644 --- a/src/graphics/stkmeshscenenode.cpp +++ b/src/graphics/stkmeshscenenode.cpp @@ -341,6 +341,8 @@ void STKMeshSceneNode::render() if (irr_driver->getPhase() == SOLID_NORMAL_AND_DEPTH_PASS) { + if (reload_each_frame) + glDisable(GL_CULL_FACE); computeMVP(ModelViewProjectionMatrix); computeTIMV(TransposeInverseModelView); @@ -364,11 +366,16 @@ void STKMeshSceneNode::render() for (unsigned i = 0; i < GeometricMesh[FPSM_GRASS].size(); i++) drawSolidPass1(*GeometricMesh[FPSM_GRASS][i], FPSM_GRASS); + if (reload_each_frame) + glEnable(GL_CULL_FACE); return; } if (irr_driver->getPhase() == SOLID_LIT_PASS) { + if (reload_each_frame) + glDisable(GL_CULL_FACE); + if (!ShadedMesh[SM_DEFAULT].empty()) glUseProgram(MeshShader::ObjectPass2Shader::Program); for (unsigned i = 0; i < ShadedMesh[SM_DEFAULT].size(); i++) @@ -419,11 +426,16 @@ void STKMeshSceneNode::render() for (unsigned i = 0; i < ShadedMesh[SM_UNTEXTURED].size(); i++) drawSolidPass2(*ShadedMesh[SM_UNTEXTURED][i], SM_UNTEXTURED); + if (reload_each_frame) + glEnable(GL_CULL_FACE); return; } if (irr_driver->getPhase() == SHADOW_PASS) { + if (reload_each_frame) + glDisable(GL_CULL_FACE); + if (!GeometricMesh[FPSM_DEFAULT].empty()) glUseProgram(MeshShader::ShadowShader::Program); for (unsigned i = 0; i < GeometricMesh[FPSM_DEFAULT].size(); i++) @@ -433,6 +445,9 @@ void STKMeshSceneNode::render() glUseProgram(MeshShader::RefShadowShader::Program); for (unsigned i = 0; i < GeometricMesh[FPSM_ALPHA_REF_TEXTURE].size(); i++) drawShadowRef(*GeometricMesh[FPSM_ALPHA_REF_TEXTURE][i]); + + if (reload_each_frame) + glEnable(GL_CULL_FACE); return; } diff --git a/src/items/rubber_band.cpp b/src/items/rubber_band.cpp index fc4a9beec..d978c3926 100644 --- a/src/items/rubber_band.cpp +++ b/src/items/rubber_band.cpp @@ -22,6 +22,7 @@ #include "graphics/irr_driver.hpp" #include "graphics/material_manager.hpp" +#include "graphics/stkmeshscenenode.hpp" #include "items/plunger.hpp" #include "items/projectile_manager.hpp" #include "karts/abstract_kart.hpp" @@ -71,6 +72,8 @@ RubberBand::RubberBand(Plunger *plunger, AbstractKart *kart) updatePosition(); m_node = irr_driver->addMesh(m_mesh); irr_driver->applyObjectPassShader(m_node); + if (STKMeshSceneNode *stkm = dynamic_cast(m_node)) + stkm->setReloadEachFrame(true); #ifdef DEBUG std::string debug_name = m_owner->getIdent()+" (rubber-band)"; m_node->setName(debug_name.c_str()); From f06ad3c78570e437c287aad7ba5c52caa19805bc Mon Sep 17 00:00:00 2001 From: vlj Date: Thu, 20 Mar 2014 18:25:44 +0100 Subject: [PATCH 32/38] Reenable transparent + fog material. --- data/shaders/transparentfog.frag | 5 +++-- src/graphics/shaders.cpp | 2 ++ src/graphics/shaders.hpp | 2 +- src/graphics/stkanimatedmesh.cpp | 18 ++++++++++++++---- src/graphics/stkmesh.cpp | 16 +++++++--------- src/graphics/stkmeshscenenode.cpp | 18 ++++++++++++++---- 6 files changed, 41 insertions(+), 20 deletions(-) diff --git a/data/shaders/transparentfog.frag b/data/shaders/transparentfog.frag index 9e0e9ad0f..b5084f5a5 100644 --- a/data/shaders/transparentfog.frag +++ b/data/shaders/transparentfog.frag @@ -11,6 +11,7 @@ uniform vec2 screen; #if __VERSION__ >= 130 in vec2 uv; +in vec4 color; out vec4 FragColor; #else varying vec2 uv; @@ -20,7 +21,7 @@ varying vec2 uv; void main() { - vec4 color = texture(tex, uv); + vec4 diffusecolor = texture(tex, uv) * color; vec3 tmp = vec3(gl_FragCoord.xy / screen, gl_FragCoord.z); tmp = 2. * tmp - 1.; @@ -33,5 +34,5 @@ void main() fog = min(fog, fogmax); - FragColor = vec4(vec4(col, 0.) * fog + color *(1. - fog)); + FragColor = vec4(vec4(col, 0.) * fog + diffusecolor *(1. - fog)); } diff --git a/src/graphics/shaders.cpp b/src/graphics/shaders.cpp index b36fd7fdc..11c4930d0 100644 --- a/src/graphics/shaders.cpp +++ b/src/graphics/shaders.cpp @@ -899,6 +899,7 @@ namespace MeshShader GLuint TransparentFogShader::Program; GLuint TransparentFogShader::attrib_position; GLuint TransparentFogShader::attrib_texcoord; + GLuint TransparentFogShader::attrib_color; GLuint TransparentFogShader::uniform_MVP; GLuint TransparentFogShader::uniform_TM; GLuint TransparentFogShader::uniform_tex; @@ -916,6 +917,7 @@ namespace MeshShader Program = LoadProgram(file_manager->getAsset("shaders/transparent.vert").c_str(), file_manager->getAsset("shaders/transparentfog.frag").c_str()); attrib_position = glGetAttribLocation(Program, "Position"); attrib_texcoord = glGetAttribLocation(Program, "Texcoord"); + attrib_color = glGetAttribLocation(Program, "Color"); uniform_MVP = glGetUniformLocation(Program, "ModelViewProjectionMatrix"); uniform_TM = glGetUniformLocation(Program, "TextureMatrix"); uniform_tex = glGetUniformLocation(Program, "tex"); diff --git a/src/graphics/shaders.hpp b/src/graphics/shaders.hpp index 5cc517c8b..50a896540 100644 --- a/src/graphics/shaders.hpp +++ b/src/graphics/shaders.hpp @@ -221,7 +221,7 @@ class TransparentFogShader { public: static GLuint Program; - static GLuint attrib_position, attrib_texcoord; + static GLuint attrib_position, attrib_texcoord, attrib_color; static GLuint uniform_MVP, uniform_TM, uniform_tex, uniform_fogmax, uniform_startH, uniform_endH, uniform_start, uniform_end, uniform_col, uniform_screen, uniform_ipvmat; static void init(); diff --git a/src/graphics/stkanimatedmesh.cpp b/src/graphics/stkanimatedmesh.cpp index 70f206d16..ea67aa8e9 100644 --- a/src/graphics/stkanimatedmesh.cpp +++ b/src/graphics/stkanimatedmesh.cpp @@ -237,10 +237,20 @@ void STKAnimatedMesh::render() for (unsigned i = 0; i < TransparentMesh[TM_BUBBLE].size(); i++) drawBubble(*TransparentMesh[TM_BUBBLE][i], ModelViewProjectionMatrix); - if (!TransparentMesh[TM_DEFAULT].empty()) - glUseProgram(MeshShader::TransparentShader::Program); - for (unsigned i = 0; i < TransparentMesh[TM_DEFAULT].size(); i++) - drawTransparentObject(*TransparentMesh[TM_DEFAULT][i], ModelViewProjectionMatrix, (*TransparentMesh[TM_DEFAULT][i]).TextureMatrix); + if (World::getWorld()->getTrack()->isFogEnabled()) + { + if (!TransparentMesh[TM_DEFAULT].empty()) + glUseProgram(MeshShader::TransparentFogShader::Program); + for (unsigned i = 0; i < TransparentMesh[TM_DEFAULT].size(); i++) + drawTransparentFogObject(*TransparentMesh[TM_DEFAULT][i], ModelViewProjectionMatrix, (*TransparentMesh[TM_DEFAULT][i]).TextureMatrix); + } + else + { + if (!TransparentMesh[TM_DEFAULT].empty()) + glUseProgram(MeshShader::TransparentShader::Program); + for (unsigned i = 0; i < TransparentMesh[TM_DEFAULT].size(); i++) + drawTransparentObject(*TransparentMesh[TM_DEFAULT][i], ModelViewProjectionMatrix, (*TransparentMesh[TM_DEFAULT][i]).TextureMatrix); + } return; } } diff --git a/src/graphics/stkmesh.cpp b/src/graphics/stkmesh.cpp index fecc72506..fe9062e47 100644 --- a/src/graphics/stkmesh.cpp +++ b/src/graphics/stkmesh.cpp @@ -814,20 +814,18 @@ void initvaostate(GLMesh &mesh, TransparentMaterial TranspMat) MeshShader::BubbleShader::attrib_position, MeshShader::BubbleShader::attrib_texcoord, -1, -1, -1, -1, -1, mesh.Stride); break; case TM_DEFAULT: - mesh.vao_first_pass = createVAO(mesh.vertex_buffer, mesh.index_buffer, - MeshShader::TransparentShader::attrib_position, MeshShader::TransparentShader::attrib_texcoord, -1, -1, -1, -1, MeshShader::TransparentShader::attrib_color, mesh.Stride); + if (World::getWorld()->getTrack()->isFogEnabled()) + mesh.vao_first_pass = createVAO(mesh.vertex_buffer, mesh.index_buffer, + MeshShader::TransparentFogShader::attrib_position, MeshShader::TransparentFogShader::attrib_texcoord, -1, -1, -1, -1, MeshShader::TransparentFogShader::attrib_color, mesh.Stride); + + else + mesh.vao_first_pass = createVAO(mesh.vertex_buffer, mesh.index_buffer, + MeshShader::TransparentShader::attrib_position, MeshShader::TransparentShader::attrib_texcoord, -1, -1, -1, -1, MeshShader::TransparentShader::attrib_color, mesh.Stride); break; } mesh.vao_glow_pass = createVAO(mesh.vertex_buffer, mesh.index_buffer, MeshShader::ColorizeShader::attrib_position, -1, -1, -1, -1, -1, -1, mesh.Stride); mesh.vao_displace_mask_pass = createVAO(mesh.vertex_buffer, mesh.index_buffer, MeshShader::DisplaceShader::attrib_position, -1, -1, -1, -1, -1, -1, mesh.Stride); if (mesh.Stride >= 44) mesh.vao_displace_pass = createVAO(mesh.vertex_buffer, mesh.index_buffer, MeshShader::DisplaceShader::attrib_position, MeshShader::DisplaceShader::attrib_texcoord, MeshShader::DisplaceShader::attrib_second_texcoord, -1, -1, -1, -1, mesh.Stride); - -/* -else if (World::getWorld()->getTrack()->isFogEnabled()) -{ -mesh.vao_first_pass = createVAO(mesh.vertex_buffer, mesh.index_buffer, -MeshShader::TransparentFogShader::attrib_position, MeshShader::TransparentFogShader::attrib_texcoord, -1, -1, -1, -1, -1, mesh.Stride); -}*/ } diff --git a/src/graphics/stkmeshscenenode.cpp b/src/graphics/stkmeshscenenode.cpp index 34dde76c3..f9c0ffe9e 100644 --- a/src/graphics/stkmeshscenenode.cpp +++ b/src/graphics/stkmeshscenenode.cpp @@ -472,10 +472,20 @@ void STKMeshSceneNode::render() for (unsigned i = 0; i < TransparentMesh[TM_BUBBLE].size(); i++) drawBubble(*TransparentMesh[TM_BUBBLE][i], ModelViewProjectionMatrix); - if (!TransparentMesh[TM_DEFAULT].empty()) - glUseProgram(MeshShader::TransparentShader::Program); - for (unsigned i = 0; i < TransparentMesh[TM_DEFAULT].size(); i++) - drawTransparentObject(*TransparentMesh[TM_DEFAULT][i], ModelViewProjectionMatrix, (*TransparentMesh[TM_DEFAULT][i]).TextureMatrix); + if (World::getWorld()->getTrack()->isFogEnabled()) + { + if (!TransparentMesh[TM_DEFAULT].empty()) + glUseProgram(MeshShader::TransparentFogShader::Program); + for (unsigned i = 0; i < TransparentMesh[TM_DEFAULT].size(); i++) + drawTransparentFogObject(*TransparentMesh[TM_DEFAULT][i], ModelViewProjectionMatrix, (*TransparentMesh[TM_DEFAULT][i]).TextureMatrix); + } + else + { + if (!TransparentMesh[TM_DEFAULT].empty()) + glUseProgram(MeshShader::TransparentShader::Program); + for (unsigned i = 0; i < TransparentMesh[TM_DEFAULT].size(); i++) + drawTransparentObject(*TransparentMesh[TM_DEFAULT][i], ModelViewProjectionMatrix, (*TransparentMesh[TM_DEFAULT][i]).TextureMatrix); + } return; } From a35b7d1e2db66deb2a1b1d4d1dfad7f65931bda1 Mon Sep 17 00:00:00 2001 From: vlj Date: Fri, 21 Mar 2014 01:43:45 +0100 Subject: [PATCH 33/38] Do not reload Caustic/displace texture each frame --- src/graphics/stkmesh.cpp | 6 +++++- src/graphics/stkmeshscenenode.cpp | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/graphics/stkmesh.cpp b/src/graphics/stkmesh.cpp index fe9062e47..b267d41d3 100644 --- a/src/graphics/stkmesh.cpp +++ b/src/graphics/stkmesh.cpp @@ -401,6 +401,8 @@ void drawObjectRefPass2(const GLMesh &mesh, const core::matrix4 &ModelViewProjec glDrawElements(ptype, count, itype, 0); } +static video::ITexture *CausticTex = 0; + void drawCaustics(const GLMesh &mesh, const core::matrix4 & ModelViewProjectionMatrix, core::vector2df dir, core::vector2df dir2) { irr_driver->IncreaseObjectCount(); @@ -419,7 +421,9 @@ void drawCaustics(const GLMesh &mesh, const core::matrix4 & ModelViewProjectionM GLint swizzleMask[] = { GL_RED, GL_GREEN, GL_BLUE, GL_ALPHA }; glTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_RGBA, swizzleMask); } - setTexture(MeshShader::CausticsShader::TU_caustictex, getTextureGLuint(irr_driver->getTexture(file_manager->getAsset("textures/caustics.png").c_str())), GL_LINEAR, GL_LINEAR_MIPMAP_LINEAR, true); + if (!CausticTex) + irr_driver->getTexture(file_manager->getAsset("textures/caustics.png").c_str()); + setTexture(MeshShader::CausticsShader::TU_caustictex, getTextureGLuint(CausticTex), GL_LINEAR, GL_LINEAR_MIPMAP_LINEAR, true); MeshShader::CausticsShader::setUniforms(ModelViewProjectionMatrix, dir, dir2, core::vector2df(UserConfigParams::m_width, UserConfigParams::m_height)); diff --git a/src/graphics/stkmeshscenenode.cpp b/src/graphics/stkmeshscenenode.cpp index f9c0ffe9e..2b4301fd1 100644 --- a/src/graphics/stkmeshscenenode.cpp +++ b/src/graphics/stkmeshscenenode.cpp @@ -143,6 +143,8 @@ void STKMeshSceneNode::drawGlow(const GLMesh &mesh) glDrawElements(ptype, count, itype, 0); } +static video::ITexture *displaceTex = 0; + void STKMeshSceneNode::drawDisplace(const GLMesh &mesh) { DisplaceProvider * const cb = (DisplaceProvider *)irr_driver->getCallback(ES_DISPLACE); @@ -167,8 +169,10 @@ void STKMeshSceneNode::drawDisplace(const GLMesh &mesh) glDrawElements(ptype, count, itype, 0); // Render the effect + if (!displaceTex) + displaceTex = irr_driver->getTexture(FileManager::TEXTURE, "displace.png"); irr_driver->getVideoDriver()->setRenderTarget(irr_driver->getRTT(RTT_DISPLACE), false, false); - setTexture(0, getTextureGLuint(irr_driver->getTexture(FileManager::TEXTURE, "displace.png")), GL_LINEAR, GL_LINEAR, true); + setTexture(0, getTextureGLuint(displaceTex), GL_LINEAR, GL_LINEAR, true); setTexture(1, getTextureGLuint(irr_driver->getRTT(RTT_TMP4)), GL_LINEAR, GL_LINEAR, true); setTexture(2, getTextureGLuint(irr_driver->getRTT(RTT_COLOR)), GL_LINEAR, GL_LINEAR, true); glUseProgram(MeshShader::DisplaceShader::Program); From 68935decaa5e9ef7d7ec6e6a7b2eb90b4bf84b24 Mon Sep 17 00:00:00 2001 From: vlj Date: Fri, 21 Mar 2014 01:44:14 +0100 Subject: [PATCH 34/38] Reenable Debug Output on windows. --- src/graphics/glwrap.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/graphics/glwrap.cpp b/src/graphics/glwrap.cpp index b9a4f2caa..d4da39c11 100644 --- a/src/graphics/glwrap.cpp +++ b/src/graphics/glwrap.cpp @@ -64,8 +64,10 @@ PFNGLCHECKFRAMEBUFFERSTATUSPROC glCheckFramebufferStatus; static bool is_gl_init = false; #ifdef DEBUG +#ifdef WIN32 #define ARB_DEBUG_OUTPUT #endif +#endif #ifdef ARB_DEBUG_OUTPUT static void @@ -200,7 +202,7 @@ void initGL() #endif #endif #ifdef ARB_DEBUG_OUTPUT -// FIXME!!! glDebugMessageCallbackARB((GLDEBUGPROCARB)debugCallback, NULL); + glDebugMessageCallbackARB((GLDEBUGPROCARB)debugCallback, NULL); #endif } From 2f2547420deb628d8308ef12464b1725ee4160f6 Mon Sep 17 00:00:00 2001 From: Marianne Gagnon Date: Thu, 20 Mar 2014 21:13:05 -0400 Subject: [PATCH 35/38] Apply Marc Coll's Grand Prix editor, thanks! There will be refinements to come for sure, but this is a very nice start --- data/gui/down.png | Bin 0 -> 4102 bytes data/gui/edit_track.stkgui | 54 + data/gui/enter_gp_name_dialog.stkgui | 21 + data/gui/gp_add_track.png | Bin 0 -> 6226 bytes data/gui/gp_copy.png | Bin 0 -> 19281 bytes data/gui/gp_edit.png | Bin 0 -> 14873 bytes data/gui/gp_edit_track.png | Bin 0 -> 9550 bytes data/gui/gp_new.png | Bin 0 -> 14289 bytes data/gui/gp_remove.png | Bin 0 -> 17774 bytes data/gui/gp_remove_track.png | Bin 0 -> 4028 bytes data/gui/gp_rename.png | Bin 0 -> 1285 bytes data/gui/gp_save.png | Bin 0 -> 5618 bytes data/gui/gpedit.stkgui | 35 + data/gui/gpeditor.stkgui | 45 + data/gui/main.stkgui | 22 +- data/gui/up.png | Bin 0 -> 4064 bytes sources.cmake | 8 + src/guiengine/CGUISpriteBank.cpp | 33 +- src/guiengine/CGUISpriteBank.h | 5 + src/guiengine/engine.cpp | 8 + src/guiengine/screen_loader.cpp | 1 + src/guiengine/widget.hpp | 3 +- src/guiengine/widgets/list_widget.cpp | 3 +- src/io/file_manager.cpp | 53 +- src/io/file_manager.cpp~ | 1087 ----------------- src/io/file_manager.hpp | 11 +- src/race/grand_prix_data.cpp | 261 +++- src/race/grand_prix_data.hpp | 40 +- src/race/grand_prix_manager.cpp | 237 +++- src/race/grand_prix_manager.hpp | 19 +- .../dialogs/enter_gp_name_dialog.cpp | 137 +++ .../dialogs/enter_gp_name_dialog.hpp | 70 ++ src/states_screens/edit_gp_screen.cpp | 334 +++++ src/states_screens/edit_gp_screen.hpp | 85 ++ src/states_screens/edit_track_screen.cpp | 240 ++++ src/states_screens/edit_track_screen.hpp | 78 ++ .../grand_prix_editor_screen.cpp | 286 +++++ .../grand_prix_editor_screen.hpp | 68 ++ src/states_screens/main_menu_screen.cpp | 9 +- 39 files changed, 2018 insertions(+), 1235 deletions(-) create mode 100644 data/gui/down.png create mode 100644 data/gui/edit_track.stkgui create mode 100644 data/gui/enter_gp_name_dialog.stkgui create mode 100644 data/gui/gp_add_track.png create mode 100644 data/gui/gp_copy.png create mode 100644 data/gui/gp_edit.png create mode 100644 data/gui/gp_edit_track.png create mode 100644 data/gui/gp_new.png create mode 100644 data/gui/gp_remove.png create mode 100644 data/gui/gp_remove_track.png create mode 100644 data/gui/gp_rename.png create mode 100644 data/gui/gp_save.png create mode 100644 data/gui/gpedit.stkgui create mode 100644 data/gui/gpeditor.stkgui create mode 100644 data/gui/up.png delete mode 100644 src/io/file_manager.cpp~ create mode 100644 src/states_screens/dialogs/enter_gp_name_dialog.cpp create mode 100644 src/states_screens/dialogs/enter_gp_name_dialog.hpp create mode 100644 src/states_screens/edit_gp_screen.cpp create mode 100644 src/states_screens/edit_gp_screen.hpp create mode 100644 src/states_screens/edit_track_screen.cpp create mode 100644 src/states_screens/edit_track_screen.hpp create mode 100644 src/states_screens/grand_prix_editor_screen.cpp create mode 100644 src/states_screens/grand_prix_editor_screen.hpp diff --git a/data/gui/down.png b/data/gui/down.png new file mode 100644 index 0000000000000000000000000000000000000000..87eda19a3cf39b1f9492f57c98e094939e5f9110 GIT binary patch literal 4102 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 + +
+ +
+ + + + + + + + + + + + + + +
+ + + +
+ + +
+ +
+ +
+ + \ No newline at end of file diff --git a/data/gui/enter_gp_name_dialog.stkgui b/data/gui/enter_gp_name_dialog.stkgui new file mode 100644 index 000000000..0c69499b2 --- /dev/null +++ b/data/gui/enter_gp_name_dialog.stkgui @@ -0,0 +1,21 @@ + + +
+ +
+ +
\ No newline at end of file diff --git a/data/gui/gp_add_track.png b/data/gui/gp_add_track.png new file mode 100644 index 0000000000000000000000000000000000000000..6e0898abde72ca262e0460afe51a81dbd3ae87cc GIT binary patch literal 6226 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 literal 0 HcmV?d00001 diff --git a/data/gui/gp_copy.png b/data/gui/gp_copy.png new file mode 100644 index 0000000000000000000000000000000000000000..4dd134dcd90a60c5db7f909797e230684c6b5be0 GIT binary patch literal 19281 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Й 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 Eg1Gs6ZU6uP literal 0 HcmV?d00001 diff --git a/data/gui/gp_edit.png b/data/gui/gp_edit.png new file mode 100644 index 0000000000000000000000000000000000000000..9de1f49b375add690a9732a392d2504c1c99cac6 GIT binary patch literal 14873 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 literal 0 HcmV?d00001 diff --git a/data/gui/gp_new.png b/data/gui/gp_new.png new file mode 100644 index 0000000000000000000000000000000000000000..a4aefdfabd16b8d14a9fa4d4779a20e6433e5072 GIT binary patch literal 14289 zcmV;?H!jGDP) zzVjU+rR3AH2_b5LOMsccICC!swg79*{Xr?^i=Ub{`7{tf2+;(57x)TLG>DHZ2YvxO zDL>&2^ob{c5aI;j5x~C(AAb<|yp&S=q_&Up2?u{Z@X#KD4{!$XcOis0AOZG2@Mi!o z0_E204MWPg{HqH%E?dK?mT;GiI4Ap>tN7gpoJmj1(^b& z7}ru^<46)zNi+~*>PR0e*Tx6)^$q6!SGw<+ zU5d9D2!|o5g496?gAkx64FaTs;Fql5*vayjH!yQj@n8wywiN?kF!wGY#QnfOf#v&} zTe;7v03pOI;5-xb(Yy0muUBQDBvpJD6f9bEeLGZ{PmD1xB~!C-`7C_-;Zl6B-#edMI9>&(Q5c z$sSSp?e#Zt$oxtUJ8TRj1!#aY9bvDewY`VCZhwQ858625um;Zm#?=%S*8{pCF*3Ub zXdrc@o?>%rXNC|TtZrw;yRB^8(!Hz4PXd1gu9H#*KGg&eLL3TQ4;(qreX6Q3f6j0g zE||(`pPk025p@Q@ifRIF-93)i3b9z6A6#)Q)zvz)=MJZ7SS22>O4rU1tKQ$n^UtoK zvRq}>L3Moo;wvbxpJ;%c20iCIy{B@nt$cSA|9tQ*?tAn-wsj1MZwGLVl=6;G9RY+8 zg&5)1Z{-U}75Y6Kf5d1`|Li1AIes!V)n$1=_BhDkalKF1;jQJb@W4Iyv3B)FbO}X; zDpi$!MvbiGn3EPTb>7K%{S_`in_%ntz|TF`Qz>3pzK(kyd6x&CTt#nxUUhLN@Fgi_ z@>4+oA;iJJUBGy^_m4hg7?*uwNEoBSvuT z_P_x zdV_IJ|MH`}^hUe&0e&T={I3DpPyr^I!6!kswPL_(DP`YY4+NQFdm*q0_{;#G3kxA$ z16~GR0$ygJpvHY07qcWmp8dCA^}a~*B30*9Gi(&50NI0m>Z zn=XWS8MqHvBBk8 zYwzJFKY5fL-8s=l&20NBj0Bc%0K*5_p%t@(eg^oX5aJQwYANN0-6nt#;&Z^gCa8P5 z)dN2TzAuDW46L&~tzrNpgaTq1sKY?t0jXBdlAueY!%GVhbYTbD2E0|wyY%`T65t?# zv>ap}N#H^N*K?B;3YJ|GjVE~c;dgoPfww8}fv)9{uv!T57vOxL@FTRnijmK7q7dR7 zDdl5>CV&v)a^QF7m*fOwMvbUt;<%v{_*4>!6g|BmR=&H1cp|U*F9g1p`#V@b5C)*0 z;ouno`Zh>z0WE3}Auau-sTy?6h~v8gp4Rh99^l=;Czl8|d%!9QoJR}zY3XGF9RcxV ziZ|cf$RiKE$L5V)BoZmS9uJz#d4`$ho_F&XcsTf=VN{eA;`4ckCp22tce1Iq8%@)l zw-*5q2_Y_*Qr@~t0tg{)1g>&kpEGj|mw)v{&N^)_g#~JkRU3=Ncx}Z7o_}>M&%e5s zH{WR^k<@l&Tew{{i7*npqd`VM>x9$}NcI|yu3I~3T18l{;neq5Yv0=YaW?`D{p79= ztdank)g&re8^+{aJe8trXOK;scCvbP2OZn{iN{l@s)CdfT{re!Z9cEcyqS$0eds8T zK5R6H&KW^LK|zj6jK($o_1L@o@$WCN^4%>?X>f}WqDe~m!@ORA5aKJqU#vHj6#Mzh zAHK;K&OFRD?3phd8Vp9b>2EJ{> z=)w-ZG*yN}iaR^@t}S23xp15dX|p9WyJ8R^mQ2#IJwV5f03ADmban*T*%6|nBS7bl zAbq`Iii^DL3<+L%-#vApti;E6zdDaAE}2VdNuf*Iygu*#(e~Fax zwj2pyCRyBIy>rZ{p*;StYneE1sHxK&Mv|>{?bl07d|dnOqxj~9bGZKIm-yd1RuGNZ zRfM950b1W_tqY(4Jfpxn#u$vzx)HJ;w0_Y0K?@q={ZcnbkPrAhA%KktGD|euds$K_ zia=2nJYE&ASHa^^QB{GeDyXV3gqD=(x^NAFUv|Mk{MV)PsH!YBL3T3P;<1R_KlX() z4rb=G5q$34-?L$Jr}gW9BZPP;?JinXz*Rs+MsXAs@bJCYGI4y934Rvbon1ki*R)Yq z;-jv*h`Q<`ydEbH46#{N?&pT@AIlXN&*8s*wTwF-TD1!mFaRN}R=uKNc&D}C$y(l0 zPaw5^&;uwU3^HL1`O1{Bld=bL(%&x?%ef2BbqrfXlcvk6+x5HxC?W%b$D`u)C??>> zuBZyCNem%O8`q%%E;wTn*MIMD8tRHICYbYqAel_j*&U*@J3=6oVDf|pYHP}E&aP_ZVLf0j`pKEKC28zLao5d~=!h=vN zKq<|ms&s<{3?^t}-tSa#18wG`D5^jxA_I7~_h)yhOLS9RDT?5R9~{nAUzu$QgxP!j z`>Q+n;jdrdl@+b@1#$ezDw-pZwqH<%u57SRS=3m5dt9uiei$&B!ezN zmy)7F4@>@XGFN?VR^BhW=K5vKJn64I{@hwu@By}L?cpakKEcS@KVs>N&6x+h@a*}_ zn?1&6+L@T62qB7`F4~ospP8|IpLy;bPC5IR1Vhn5^-*1y+;Gc_T=t{KkU3F2iIE_|PI2 zE*RrtbV{U@T=wIo-0-K@2jwvU1Vb@SIq!dY=J{0_b#Uc3PIBJ%H6cWi0vu%x_6#3Z z$C;-c4yJzk*42L{9?u!3jsqW>fgRV_^UZg>!B2koe6C$!x=wSZ*K^6|M-5<5?n1_9 z{P01lja^yzVi%toEJqz~t`J7A7mCQdR!k|NOStmehcjnJecqq_$xW|v^WwMj{b~=! ze8yE~mQfa+l zs-=|Eq?F^Ol$C%FI2EwzhoAr9C6>Lq$>KxXx5H*vgBHjG@Bq8ChGpfR6Ja3dV^4_S zAc0U!u`dLQz%B(8!Hk2NIN{iFd1F$`UTfv&H^1(@+zuQK_@tE8QpyQZ%Bki!%TKLO zuzKxwo>=-GAW>C?la8O^{H}u(jQsk{He*^=6ut9)P6&Cil=6BhW!N6mmq|0!coYz@ zs)O^dc$m)aaBkov{fASV?SYjXu${Y0=+#Dqd@^}AhrMmjxn~l=(EVg|zm*U^pNC5? z`;7Zn3+U{LaQ>A~ImLcAaDTFwIVY ztXS^t3p+osV!;0`C}9 zF?JOc+KtU={VO6H`>3oca&HUZhCi+3>6f-RzwUpelur-hXMIkCFx@dmk8rx@lq*2c zdL6za zLhiZt$jc*(F}Q9`r)yg*eYuq%{`yttN1p|LGRU8qV|_7wN@M2v1Hnj+!=RXYC97n* z1DTdNbnXP_2j&YQJcEz|N#HEgU@-={HOblEdV@iueL_lyZb^m|$S;A)!J z_3+#)9UvNWbv*e}vYdchuUqevd8EN$yp4Q=2XgO<1K`#_zQ8^Ae?arv9v1&=HSR}bBA_!6~f!?k)vI; zgNuEj2&9Cc{^CWZ9B~(4{MIuBLkZ`DTqvddU~p1KlsVPJxn~}X&*#bfhnJSGaei#G zf{~A&*&co3b=x!i>_=x)=(pJw4e2oAAW3i^a9uvpmXosALDS(z=u>VL(`Ot3s#1FBg+86r0{Op=jZ2$Msf9C|0mMOr~K*l;)x1pVu zHEkI~uyIHgSAO&4oF5ZHEZB`d{}YVdwn*-9upC(IV;EFUwRCULn|`>t!3?Y)@|&t-Vy|FH%M?R@Vy)fkOQ*S-;W-M>pl&*uFK51)a|5jkpP!TDgUt>;Ex0Tg=J{Jwz`CCE<4^98@c5l%bXv5 zMoJk?j|SSJM}NNUDcZMnXBwciq<}yF_7bNhTZxftR=K$mYZNS9Kf!=q? z=W|K~D-q-#1`hHLFc}g+Hv!kBG*TN)@TD|{!(9l_vm4-70FRkiWX2!<{OiwCTH?zX zAgx=w`SaqO(uen^dosWhEE&7;c#{9Qafz(~7M?tdpZ{Q?^Gl`!{}Mv@1}hbFC4fWh z=jeRhMb{7D3Q8Bx!KMZnMA(^KyN`K?oUw^~O_Lc3AoX+}U{*RfTt1v}nO(Si_W?Fv z{pZ)5$ifBFY;NmMfAcsAd&2HI6LW(AW*t16q4u}MOL^AYH}@^g)P!H_1SrVd;Slzx19S`_H~MB!G(z z5Eumb3UC{6g7x}&XUyaW-#XUv-{7^C8@S`X<<76TE?xW3I#J^uV0HRMGO2Oa7k^7% zU&Pi(fBW5q95Q>9^9xT0?lNbwd^8D=7f@{g6`;9D4M*fKCuFSW88aBHWEce7HEBu# zouXn7s*;}GU|bjOHb7ty;Qt+C-auwMWY#eL_UlEq0kNLGFz0;rAEfLoTWw;EjR0n( z>DyL%Z`#ty#h2Mr$^8W$9=iQ&j2u?we73WJ+sp?X903eDX*2w!uCZ~;%cN4#0mQu1 z6&xT|Kt5Z*CG3nGAQXuGKG*Ev?71!t;5AK02pBrNn8uM6Nb|ZbB|;Hs+TaOb7OeaW zaFO-;$fgP&`rCQ<3$i(cQtEv1@;hm5?{SK+Z(E~^j;UI5DUQtGho4xNGqYWD-F>{ia)YfAMh&lG*^+P4P+#W!{8xZK z3`PPdLgct_U45GvF=;;jvPzoQJxTkHw+RHdq3a2EBQf7KH+6uXAM|`;-%S#Tf%Sg- zjvm%++(9HMX&60?X@{IaNyQM#tDC5|CkdRX5D^2!C9I1N^^`$Jk=R!se ztF%4i@|BzT!7rb1e*R?>TLvV6nP_khu+wS}oc-ln>FEjE#_Pw9tmgS8ms;4D^<~W& zM!S#z*z|ug6(OZ3DJ-sF$cX7onsW-pwL|ESZKY+yzuDQ{GO!nr&mD9|2W6fhky}r9 zl?u2{T}Y;qtlhi~U+E|&AH0YOGe1Y&uqo)e&W_giXc{$*Nz)s%ssTvZy?}hZ|0dv@ z)@ws*i+TRu3m7}H%0|xa-Z1A}dY4o0?*z_qo4T(IFpzVewa2z@?Pk%JZY3H^*nV*Q z=o*&Y|7GfGa^{d;C4~6-E{#A-@9YcgplZly`nuZK-ufOwQK_gI&aiQZFlo-I#I%0e zw!P`D|8UKbZre(GcMrNQ35Fw7R5#_uc6+x|T4mI85UN1aWM*Q>K>A+@aXpTCnl+V$ zJa_jwOd4BdQ%})Yl2a~P%nPq=bbja0oqB&z1W5affbGN2zSP2LU;GQPc+%D|Qzz8( z>_0E0sv;+8=r=-$FT1ILp1z0;l#~|7<1Ik-coB++WFj{e-Rtuci}$gy^<}npyo)II z(llW%O%vu(Suxx_cYu6hV&^!4zDqlxvbcm;sE3+{i8PLx$K<(-@KugvP3u-X9+hyw zUXvjf=_eiyQdCl9f~uOHfpD7^7O;T-GvFHQn^hHlp1pGsQ^!}^48CYI$*C9L&2z8h zHXX#z%QBjstp8wB6h;y70pu0D0 z^Z!$+Bp$Ev*&)NG(Yj{2P2I#}L88%qwrpET?TAT?opC%KZvjG4iG}+pEGTuv4CF36 z0+~k*t<*M5X57qE@fFpuWzEZIDMK|R5)n4Hyv&FRhi5ED zMW{$AQ58t$@c@Jn*E$Whin0QhE?Jn+yJOjH+Y$9uvSMK?tL$ z(;L?7Pd(qlnP0ktL^5RqY1Y&sJiYjA%1V6B-&yR8T=n!uY`syUlS~+Sh~?Ey_>0Tf zvSvA2DnTsL&$@S?p}eYriL+0kw4#Av-wrz4SL5~iDXSbxAl&JaM<56A2=sxy0~Zz0 z7YkEfI}#9#o_rMTn^q7D_u=stFk<472t}b~<-=@het``iJV(>$Irxi=w9D>}jdX15 zAs$O16oE@Qg@6#^GK}=@%vM(7(u~_DbvZC!Sl)qOaaXJYiP{b7u_YsoT$@wAe-huhW?6>5JOLbz{{S+Sa~GIAA2e zG>$%);_^CHzx6m9KX?vBQ5iaN7D7=-#6q;Kf1S~jk47je{-O$!$taqZa2Yo+K##}6 z$jp2{_j&)TTHj@7pu;6cK_Za^<1tDrjjHq>ub+_!odAb8UFgyhA5Y!6khwF4*lvtQlbrt52Y4nI{FT6G z2U~)6(AC1Fl>O%VJI`UMVDZ?})tq$UVjlkMIg}P>r!gNoYbcNZ?Q~AO_`#fM;aL@6 zN&r)U_5;3F}!jpg<2DeA{3VPTGKC3^vlv}Jm5Cz`rLt@YBO7KQ zHH~o4kPdUYU;<~ReX6RfTpED2pY#6%<#!a z(HiKaBiNk@{TjIE{+Hs?8lvGo0zF&NHI0(O5)w&`cwDEiJI?mj5PdySk_nAe%IN)y zB8aE*M-{zZg(ZJInIq>l+TsqK-4TvH_kK6<-vf>_G2&w(fJuTLHxjITe;bFNc{{D` z{WhE6q@zc%_;;VpTMQy&2x9ly6(LZCVh&g8cs)Mqt4FY9%_~ShLLm9j+_ zJ9;dGiam_Ovl%#0*l&7{T7{Oqg{lVW2x=_|+bdN-Ub& zxl+j({XK1LZh3{Fb(JKOI*CMzWFkc(p_#8Wl1YtJQbSchRUnpUO%X*AEdJwXIqBGu z*)axSOM8IBKYu^(GT6uVU33529;HQc1g;*xRvzqJk1Yr#G;TK&+3^(~hNg=N7tm ztf#hdGQn6s5u21SQ>H^*XkIT>^<(gO3+U=tho&VNIpr9-;&B485Pn|)1wx~B?JGor z-Sl>DX5)v;2==zqy}gBTqlW+r@py`4LL-sTGDJuvwahg`0w~0i?h@dZUmeXk3&+`l zNzLnen7`*AX89$DFc-?sFDjGr8Xj%CfEpI)_=G8B;_N^x%o}j992%&Hf9sRA0o^}jE zD0FwMr@VSN2pBo}GqiTCBc;VNq>IMGbVYg@Ir%f0V1ISp7`k_?M^RM9Ogo;f{XJ+> zXIOO&g<6Qs&Ck=-zM6u7;xV)Lr-_i%NTm!ZpsE5@fw+qV zxcwemIxR5PjUQ6>js+ngbPg<&mz{N9CyLL_;9V6 zR%Q;9UUS(TE;)Cyiwe+lR6uSePUAeD2-6kpVEDxOjGuWDjiV1Hm5AX{REF1$1F6y8 zvw_jmjzd+wM1o!Ti^}kL4UfdOGf|YRN~x$DPOyJ_#@hE6 zR-ow`LJ{-IRy&Fz-+cYi z95l7o?h2A8`Gee5I(GDKp=sPa$|{@i`AaCR9Lm@kC!qKX=?`@QI=Ys`S5Ra|;sO*F z*(YI^RyGhz#*FL?B!O6v(u#UVs-I$u0wxk6G^x?ru>%c-o{kN4w5=kQjGM$Nrm(n% zNHj(=p&4o*X{dmBJV`v3B#}s=Dn{>DRY5eKCBT+zNFrJ`k>>O$uC!ne->)(Hx?OP0cck>%We%klQ{fI$rJ?g+FoWVBJwqJ85^c5ZK> zzG(`R4?dkK2QQ?mehjMWK}FKK_H`2RF#e(nA|d+%4O%KeEE=G|W4MI1B~@&0et}rH zFXP5`wy&n7td?zC-eLQuw;54ePhCY7mGxsNt8AjExQd#FiA&wPTDZeL#sKzoSY|GXl-~Z*S-2AJfG7LZI_z_%q_5_OxFvo{;j@f!Yp=gldm5prO zumUM{Mo+eT7Inj?viXB&88>7Sp=dwr-u*YFl|u>lcd%{KO3JE+6N~h+vu!nv<@K2W zPgy|`9#KPE^D?5zL}vMbP~Ub+{Dn+z8ir5={k;*2s+{Xw2!@Y8gtql>66k5Cq_h@I z-vVc6Jtxor0ONv7f; zbxQmt)D%|}?AcCnNfn7`h>guJuzk}z)E1XhURah@EvY!2eLIOJ<1~#qn5pv?G5xS} zm^9~fq*_QviWsvuh;P7q5N{{HF58wnz* zq!BMjX*ze_*W&gQ=L#Wi6GGJNlM+A(F&Sf$g6nLdA$3LQx#+Kq^?mjbgb($g!?-v@)d*B2t@h_=_w}7U5KJ&la)8GdV!u$7k%Mg znyMP;5BJd%?!)gZK=l?74tC=&uB51>${668{2A82^BA?2Wt5c^v!QJbrIk$-7907H ziCBof?pA_5+lWSkR8%)nSmLz-ACDUYW}dVJ@F?_#Qp}uOPDQzoR7x{QkTQ(MA$3L0 ziJ|}nrm=mb5aN`*wW^3`FM(fyF}8N_`m=&6nurh%_p<*r6bsQG z3n7sdC_XBR%kg=9fTU%|I>ygF6-D*Xw*Czg(ICSo&d2L5L`x-TdFxS1Jw77w2)d#& zdEQys8jw<_ZQYv`_=~6?KArXrD=4Oqit>_dDR=>qNP^y;FnxVddV3=D_C)CE4%^Pt zd!b*Yu+T$cp-NGqhox`F`TF_esIDnC_SzC^YD%c8DW<%v5OnD2j`8=qo4Mt-_u1T* zqk8U?QeOBUw*ky~0B2gSU32Li*1vc$-~HNbsw(_8-xdLyZuI_C$`E_&Huhxe?h0oy zDC4|9^-xt*Zq(>SM<6Nn7t>Tx&#;Oi)Rk4^Q;jnAsZ@7|gwy6yQQkpz)&f`|!v)Un56 z*>jHxzUnd5KqxAS#x!(O@28TQ%@EWbQdv<1zx?50Oq-Zv{|((22r$WJ?9~=!hkqT_ zuQDD$O3#p>vn$H8uWqxchqQ>zO9bb1Fvo5zHDn}*ju<~5)st)Lg!*^jRTO+4FESOU zyJH9>H8~qJ;t#&q_eFn zL3dvQO_L-|NtiYSQz_XG_C^1i2baK%L>h9odmKw2I6y!QX!u`|NUZ{%clZ~c!GKuWn;2+<5o z28hN|9D2sRJhgZs8W75-w%YwFk;=tT&qDlDg}qLfBJlRB!ZWY+qL z#A0k{T}|VVN^~g+hvG!S2}0&ELjsEwNT2Jvq`;>d;G5##>rv?HH+uNy_9%b7Z!=e4 zI>{I*G}S>eWuGv(c4IFmp7XHNBWRXVZr)eCfRmlW!lflX&Oc)!UpjXRvk$5>8dQbe zK%6`7{*c@LxtjNzvjaurMpv-*l`p!?&lJ0^+vIqyqqDiYgNE`d{N94xHKI(_(i;rY z8w$`^S4)AAQ*@d=#LWUj}ZvO==cJTR>=OhqNpL5QDa%$el$ZG^E%C8&b3gHf`=Sf)FFP-5=(rKV3HI(R z{gOG_r*>`L(N29;HHCivfNKGT!f`@@82$Y*0s)f*{Zab+jPt(U2>pFg(u4nP9o<}wcOp6lnOz1J)DJrs^-zz>@}J)GXd+4D#w zLCEwBj0GXHfP2jIv=n$}ZB7EuO=erTE$!yfLi~WQ_f>4}bFixO9O2|A0`eXE` zk9`q*UWI}J6@P)D`~3x`9pG2#4N3lRzb$uD0~)229cEipn!bI=1ixQ?%m68T%$u1l z)F-8El~Ojl3jEkM%5nc{ zBj_=Z4UkX&yV(NyNMkwIq;6RH!8pNy0d_DLBN&Vk3dU^Ur)>ahKuFWHEbv|hug^{b zgm5Fk-T&$K7yl0=8;qJzf%ntFB!JqQLRLI?4kLz@?sgm?zc-M3)TJaIPiErz>0{cs z3#Gv}b-qCklN6crxGDA(3JN^tQN`~!RDj>F^5pU$uf894>i5RMOyK-POMtWofl*s1 z6CK&v73Ji!AECQ9f{u$p=w=K$cQ=Dj$@chYERoUU!9cd>8$Q0_^M_0+5DXaiq{V%j z7@=SspD!);RlHtd9*yv?*Q>CuE#U+ojQqaP0SRE21Xo}zTc6n$96Ox5{(L;X0uMf) zipQhc*54V>5OhcYT}l#(lrb1(PTR_)ZpDlQj&uw^6XuN={=QY$2ZL#85JOc31wNz4 z7Zj)z_{?L0g1+x!4EgoHynUS_+G7G*jVI!NnXH@}FbF4ob^fA;xy5|Q2F;oS7 zJ{6x&&4_*D+#rF+E4b^ae%7_c?U8CYR7&~gKGEL$AOVUo3LH$aUO)DT27dF?!zeBB z+E(H=^6b-#Gnp-Hb~a7ZGldzAQMPOzLLxnB!_3={$L%&i%(Mg2k|1sHg#s}`p*V^O zet}Ohjs?bfS^`L+aNl$NY}lGp_3gLj910E)AU_Gl054%IPLbKBO{(PgKR=Y}D!*+J z=JeWpwr-{y%dZ&{KsP0TZjQL66EM>%Aem7GPGc}?s)0}_ZW@JgR0X_N@Xe#oXNdh+ z3hsWopN^iKe7@ztu~N#|0Rjw;1kVB^t=F4|7IWSA4r1ci3R41@i!(b2V64n7GZniv z%}BROrX_$W{?ijT(sqEA1QAPkHxf<|4krx5&IH}-%_Tu^Sm&Ol0RrKiXzlClM&*uA zJpoJ-G?^r@l|)w+!J^NN;DR$oWo!b#W*nx$x2k}2+EqFuDVa=5fo%4!T?!EB@ zSQ4Ml@augx@D1Rl5WLYGgqE!-R<|Z?)4LtpMvNuj zUft*0bblg%MJ4CKCoh(rMft zkAmt^2t;8=uTFcnPG_Gs&;p_wMm_(l_f?U<-xI(>gz*^b7oLw%gV1%WF7wb(>!r3r zrMgU^*eB3+!zE1j{6r#!5D-ZkJv^2YL=zB>Njm!_@#HSsCXb3LHM8n6V9Bk_pY6u`EfYB#*on%_-Zmi!Fvxs^yn^BSZH90X`ZL zhL}d;*D(@L@@`)~vx!kng|^6GDy4JJ(>vMPxy$gpG%fqPfF*llg64m6^Y97Tq?9{= ztIZPYX9LGztfZYYWBH&Nb(9r*Y%XCcrPDAZ>_z}-8g*}*{`><{%C=8RyM1B^U?D;e z#@af!nv&rV;I;JoCBZW`O2r$D*0PAvtXxa0M5}hqB_`Nwj z-%5xh=jE*Wb@TxnVE^5Yx4!tyye0$ajEImqcEAyH8$MM8_{2&8b4r?x07uPj%I*2q zT^G!nTu)J0&>J!4%b$iZ*i*Qx&&;1Qj8`7G$OUlo=yGk@@+};D-o1l;tZl&azyrWj zA9WRv0|dwgeV7UQStjUWke}ns1(W&xbtlnKUt$5=(c|+-gC$S4aP=>jv8iphMtnmU zNvunNC#95;0|eM};3s0Fe=Qg+=q1HI4xK%MQ;wg=!V{;^G_-8cfV*AOb;Ex3{wVtu~gwwUyW3-bygM%OsL#fwT6-ha#SmS+ zlsPkov*?tWELuTx92B^GbP@ql*Lm~Zc9uN3in|_sj~zR0iNQKB zRZ6+~009OD|6m|9b=vDy`O}RTa>=M#U)H8sD@CW%-Jn2=UVgynw+Og5|)AIMTVM zOd8I2FF&2L&zMhPp?CMK|D0=NmXF^X#-NlsOJ8i}x?exR@)bFiU!RgvE;vAd!IPj8 zcnX;7e6O;igp1EVj!VCEBICvk%@3rTAHO?R{UAxy-4o=tf4;(>ZhwIdn>*e9&Ng74 zlyd6<0_+|MN--9qI5N-s9C6qb=FS<<^eH2mK6NCM#t*~iQ$JQ|(6O_RRjb=s)zZdm zE7tSi<0~@j1G#J)F$zSq90&#P83_t8;^~(&i17fg*Tcl|O-!FUib)fOQB_$&X-Oev zrG>_^%zj?r^%9B3h(uz9BXPpvDB*CNNHqOAMpt)$=9aCjYH4Ft%T~I323_gpVc;uL z%C3F3efO8)U?IdwdrIP3JxDk@)*+ zHNa)S8eoyT1%Z9QPItQD8ZUdBm zJi@nbJNh7c{dC)J-_DZY?JVL}xw^bO`(RT;IiLE-X&iCHY8Ee=Ls>wuXXj3KY~RIi zZ`#CtPwXvrt^Wl+^A7_2?YDAZnY&#HEYJS%EB|sLSAF(;yrqrctpV8!$q`77I(vPD zmv;7W+QoM;GM31{_DjGx&<(tnwT=IS0Dt{f;r8%lKzr7{X}T|bDwslJ6kp9`GhZpR+X zy_c4F_>U_#fRz9%3Z@3a1W9&|nNeqt0g50Vim-g1hZlDyXLI=`cY9aXu_$mia075Z zVE%&uht^hPfxh-oFS2BQDOwPSM!`@(*dRwilE5|`0+?a&33hi4^URZbShFlRTLKho zLEydaHVWJV+z3458kYZ10W-9yW7Thvr_ z)3LZ2;t~iC$R6kK3j^!~_yoFY@ZGOJK;Pc|eC(gkXW`1@2uEUs!!g2<7!wl_wsrRN z+O}R^+t$a{&OySF#KGL+UY7{Ja&3u!5Fo3)1Nahf(riv%y0DIw?M<{VZlq&r6YWcy zSv0>9&Es)&yCi4!DE!a2f5;I>S8~)*3&4=Ta{HnTXo8;pajyUEbL@Dnmt&7=;(Z_g z9R6TEU^@~cy;*<-!baF8dwYh{gm`UxKQCrbi;7H)xE&--( ziXwCL`njBY+H%f**9zKN>l}b(#RWQhQ}f=-BoawJ|CujQU2U`Ou(`C%sYKNjMn@uS z-7>(V4|P&muCR7>9T!~s8OrOII$)T;1|BOO_Rr-*~Rbg+RSbDZe?O>T6OU| z;GbN{@OMFg4Z!ul;$j~jf5aR<`N4IZdfXxc0pE)JrfxWL+3}=yuerf_Zh;`f>6QZ zz;SPP0^AFnmVID;YdOFE-boyH*c@=5Jro0dCSa%AWcOOdfS27TAi3bXB$$y1S%e^u z820RmvbTSd(=NS}9eWA_i5m7pvOAk#q^I$@bODm^50)P zhM-q-!I$z#Pz-!IYd=#DPraA3fR~-Wo$mShk3fJ(jRV=v>jEO!S>W5#*b6gkk_|FU zgp-`};k$X_x&G`M_;29fXLyD(u+$AcNo1Z@0@w~r{v`rI^}xBnM&O+@d@l-Y0-gk( z0G`StL|yjDr=7H%Z+vMZ!QiCRdn7;-4mc8SkIEmMvmIcuK`0qdkqFpHS-$)b@Dt!_HzJmE4IyO8%jP2hNf`ob04&obfh!_x z+u4Mb+r#t4dJfo12J8Y|KU43|7HHW75LpBeMTlT!z0Xq@WC8Eq7P6qXf1EFW`EK@) z6+{~?cV7iu4t(4VgU_bPZU=p*8)vu+_#Ck3PzdmD;3gN;H)8O875ER}I$&4M-O3Iy zk|cvP2b4Kr?+3F7ECX!ea9U&$!IpA?&H+AK%opkP1tiGVf+A}trjZ0i2vGF3OA48+ zO%PA&+k`1FMGHtE3 zEM3%y*Q228CgT$kUfjHoq;AgS^MAC!0Fkmy5y=5moeOF!*aKkf11s(jAtn8Vs~T*} ziQ^XqJf-L5X@HjwU;>FE_CU5I$lqsypORi#pd&%jFnRv@F7CSHRrYp`qU$D_s$z+P zdsyRc(>9-1Wy9(@RFwJgcvOboY#5S$6*E0pJc7)2nAgfbRkSl7Ddhngx97 zBd7A7vk$}XRSK-yL?Xea7xwVz(>r76|PN-w%$&1`{6QgORfQb3&t+I$c(u!h0h z55|P^(CzFFno<#2$H>?B+17q`@29vC=;){7>L6PZATpXnA!Eb1yi1xUqa$IuyGPi* zeTbpKDUwMOMUfFgVB5}ipytsO4qwyE@kh3C{895ca(ydaueU%Y#*-F*_~T}N{OiYf zar3@>Y4B5=+{iD?hrfD~@BQp4LXqO5 zYG<9gmb-5J*BLDNJXa3WQ4z)f7=w-ku%#l2Tv85p*&RA(KU@DOUudL_RSGxRAwa?~ z7#a*Qv_Hhq{xHKsAx4HG3=M@C-XCUiB1$lzF%pq@Y)kRffwB@0|N4=``OIa9QCi|J z@@%HFhZKH)$4h+V%4>;43!eRDz%|7rKn3uf>@yd%HFD1%zQoc+jjm41Gm>(iZtn5Y z5)WVc^l^OR1MB(rPoCiaUHby@M6QaEWod?|pKq;8fEUy@&=xp@G1eH8m;!4G>?yFq z&UnAD9TH3j{GkvahX^7=w4Bc}q>yC^S&>mS1x=GtRRu+nP!t(OksP5V1hy>|4S|31 z{j2%+j~z}`WzYpVpUGJsi&Xf>-h18#)~smd-538ad-e`zU;g{9N|+>1$A1AQgVs?T z0WS~V^G({9wq&_q&Nea{;>FEfOiV_wEepR_L6P&srK=bHUWJp7na8CYS1>XbWy_9? zXR>T@GZ&tBWUjA%z|A$KWf@5>0i_a@ddGIq<|DKP2&Dm`R3dpwK`j9_0IDBUFQ^)* zDkw6j5~wn$3Md*VsXOr9w6>JqO1#|^;$#HINDI;xCMorrMkVwRIVljibyN|_m z;!*c?G>NLpL=w<5lF{4mKW`a#|Ke=Ud)GqzK2OGlP4Au8hH0~ZB+8zB!Z59Dd&~qL&6WVQ@l;io0kC{oGgp8ALwxY# zIhy@5z>k2Dlmz&i>pKE)_=ctY`1}8yzJUiHeTAb={sIp?vW2NotmywvBu#d9jqr=> zpCuMIIQiK5SyCgT8jf09&&s9M{PB@47Agh4c_6N~E3KIZJ>ah-U~Kmd^>j7Mc|y83i3yRajpx>Clh z+w-sb@5lJym3PzGHAW&?+}oN8#d++RU0nU!Cs@C_l?C(aK$2Otyp{VNevSUYiL_nm zO8*CB*G3*1 zRFrvUam75nm*Xu+Gusc6RK$2IV%Qa+cMt}`40c7f&d=UUlH_a_(sh ziWr>&Aq1cNuLt?g&z_x?#{dwHBslYu|HlK5ZcW@(&)(6O9?yY5mH2F1w&@27~edGB)jp z&r)q1$ikNn@SWLmG|$bIBu5oUvXp)=%aUUV3b^vq$FP1?{j@*(n|$VrHz8cooPXwe=C;)2M{SRjfz{bZPk+~8D2g*WcE=x|p>tP%{*V5L z8?9R5j^|gpBIPVM+?#G+|IrgX@^p8W_|0uct*ZtrG!4KrY$7`>D-N9qGdUlJLWDdL zNU{UABuU5;xl%xuShc!^Q%+noZA|KsO+9@5C(q^|?gx%{pLsQ~#2uWgbEB^5JJ`N+ zkUu^6Dj-l4nbS{Rm4B_(GERPdx~*D~5k;?TDF`87=caE(^Nt&CsPQ3yq0Ux;Xeg=o5`6GRoaNum>m$KiCjz!Wpv}*aB{Bu{x`9q8I z+Zxkfh{Y1cV)_Sax8Y31PPhA?=;k{=e=biximXxC)>99;GX0OB}3;@g}Ct z$XPmHk{vaW3V&Bt1&Til;5$Eik^7(6mw&l$;7l?(VB|R8AWU_P`K@&Yxg9bP&OQY~GGn3i zlR5IRCHY@CdX`Bf2Jjx&U~vYyEy0CX{E6+m$8&p@S_}A!==yvzOap!~Ti^g;>HqBFLBoz*P_4Q8jr@OY~9NWHooD2W?UMwr;ce-(A7}>K-@HsO3z>(`0r2oj+ zcqIS)u>t}Nk7T^MwX53jdNuMgdj4sa|9ikqBuNJlCZ>`ce$Gu?cl&m>?HcFdr-ndk zF4XZ%my(qac=qw^^QH^@%*NZ8Zty_7x#9r0`p1v)`&(XP+s<*W`@?n)KjUU5rg8>v zBJMLBo#iDz0;F^}UXRM!6?4+}Jv=&9lmL6Pk38}8Yw17i@u*z9@tFKC{iho|noWBN zxHL!RM0Gy++571@;dfkrdv75a*8o@$Ob=$Jfd)DcSaBoW#s4p_F$1q$g+eG z@YQcVPRFs=^T8_~ARN*2Z{!0w8Pv0BW%+92;`2A)@u=zl@WeBn`QOC`_QeaGL5WOjV&5n;WXQ z@)Kti`?1pw;^w}LliL<691a#Ui~YRO#e#&)q7ou6gQr+b;ea;68z+V>L=NzxAe1X% z77`%rs@ZQI#7~`wQxGE)&i~j6G&feH|J#n8gY4=W%049q+%LO2`hgtX^s9Sw9>P_h zK9ky-(j4q?=Dr`y?A@z@e=Cpx1t4ep`(m2jJq_^rIeR(wfK5TFmza$tI0Rz2)_n?a z+ZO59!pE=X_etyk; z^bd@sA3$k|m!JR7W%-tDr7NNW2XV7{fqPv5P#$PACSK0;d69fRQDo#V9W*+`z`?Ww zOpXMwUBGQ2oYY1ad?B3S@FD~_n36LUZUnY6d;H<&-@Sm+5>MIy>FFEe=hqdKKD;T_ zlflU$PiO2VlLp`T-mN(g;M_CT^7YT3n}3m&F8e&Qm5PNDAW!TU=zP0~uAjjb6h%CT z95ui!!p z+}5vt{~q+*gxy^(=4k?0`G(rBu6vM|Ht)&lC7=JyySZfJ;rTamx_cwDi4Rx>Hb9Qp z7llM9ssgg~KlTisf+#XhV9!GQ7n2Odf(8dR{8F1OGB!aavsvJe((W~ zSl5<+!EMUib##T$YDXq*q&z0V78KB7HqeKD+O!c0H zOT}$~&@90JHO{<&bUR}09DeoPjX48iD|3=m`gvI zn@aBUs@(D0k1}sgRsM}#=(19tH31wsnPd0~+oEgVlbB|F1~H%S3eFHKU^-i%NZ1)M zLnx5k^NMB%XI|UF0p7A~BncYl25FvGfpCx8LLkW!mUU1BkZ`iLKa_oNUP}dc{OS^X zUL}2>LfCxpQ@^99e>`7&eL5S5e6SuQFEjYgKfTBguKsh*!)T~0PNWe!PoPz z@f{aqrXxTq0P$q@fvdi9Gta)bC+8uw&8^~*TR%ZleOdnZFULtZJfH-SC8@xD8=dT? zb=lGQ$|~8m>t6czzf34RfNkr=jl}7W6;lV;(}O;p*e@mtq?z@8|Ne1yb?ql+2%6@v zV8s#VQBu)Bd36ibwf?kWD9KX33ea(~t%|iDlb5>JRA0&?zyAPjb1HN0@tGI9`TV#3 zl>hxFU2HiJ0Zg2^WFy)3z=a>an(^^y&UpR8dDT36>&LR#m*dNR@4ym(9Q|*YF-+UQ zAFQCEbtTKzpGmN`fvH3vJNEpUk+B^!djZqAgZa@xF-?%DSWho16)2hnYM2H)_YUAG zZKHj|MwYC3H+6G5ux*?DJ+IQzwt{6Vn=`5bFmt_t>3aVUflp)~Xs8YH=uPiu;k>FG za*jS_w+kp2ts z^#dA#S-o>Iw4bVm`Am-XGT8GflB`fsGnYAwj$qmPvq)G|^bR~sk^h91ci`Pt+s zW)zAd9>~nb;9z)+uCe`$geOSoxifl(#>3RLZlJcMJx5A}C;O3*36xe*QR&6D?CB&x zF&FTAuHMd!$W;e;Q)Wwmls%9G;G<7Uj93OR1>}6bl(&IUNCFQc@gk?_Po-nfCxoW(~Hjg4Vj zNx&wN9A|iRJA3v$%C=qiux;l(E-}Om9>Fwj;cV0b0!>m-g4IZs^B9OF*w))e|JXRT zEeJDn~uV69#8~ z=sF(T)RlkDujlLi*%Bb-FXnvtp(l25_6L7KB5CA2nB`0AdFaLuP*qWoH1vJo!^Kp< z_+%^x6w^wgYF-poLy}bty)YGB^Y}<4C+X^WlD?tMNI{L3C5O?n(Ch=y`&G9+SCB;#QMB~>nw71uM6 zicJg4CcxKlO30;KRfUg-ez%e3i>q@CzIfc=tV?g;;ivW#>%?Co2G%PIp1xVvx1(^+{&uS zM3{JdihTnwQro(Wg{w|R)x1cuLLxee-&>j&GY|*x2*fmMV0t3N^dy1OYCy1W=QB)= z_fgx_&Z0GE;R)2Ruk%SP(@_n&9%Jv0Cuv=BOxkjkB?Tb_iVVi|9>ACK4YrChFArXK z9;=qs=766_n4JCL-}6u*_|Lf5*>8jZsicI{fakIg-uLJZ&i|)t(G4>Pq_xW%xc|Bf zDJ${h|IT&!k*o2ESWa&g*ciH#hge?Sf-hLczRqW`Or1n*id~x@puDPyrR&b5w4#ad z6X3 z#KV2RJD0;&HRPNaj~krxk=uBn5d0TitY%e$_F$@oPq{t#VX`V%{PDr`r*hA=XL01Z#wNGi)3nAfxlVOd1Oj&ul(^-)okTRSOOR!ck?&H;8ZJjjmTP53J7sHtCs zCVQrp0G4HfB+iE|=oY5w^nO{ENSf0}6*W!f)?c2%I}UHoi8~CB#W?=rTZ)1Is*BCU!2o)& z1W1in7m(n^Edv~L{%`5&pUSZbPCvel>wfUAX^TOm4MB2!c3F~8B-yz^VWX)Y>Z@DX z*ZCB}wy-P%SyA)StjwI~;?P7FW6}LAUVSPx%}eq6O9>|@rp+(x8}Fr|brq(OAUrY1 zlC@_M1;%2IU#+SNiFje>GK~aNPY3=%FJE&g1OO*X^*Ee5fa%uRHOA5B zU&r>{qd5_?i_TucPrrG5aW+jxr!m$F_cdiLl=`aJ`rPf z0HMSr^H-e6#K=Cz_V1>)xt(xgidc@6a8sruqga|oRs8}~&CBS}E-cGnUdIWHCX<8` z5qusmUdf_o=TpSOV@wS1rR()a2v77gHn@XD^BVw}WYWaYEp**V6TvjB^f5;Q$Rv#7 z65yx*eLNSPyC^4^v~AZoM{m4^U3&}W;y&eKF?uLKzL7QY5^h$>Dzb_`hax&R-Lr#J zj+sY8T_7#G)~~9=?^k)~$=;lcTyVxR+U8c|4-rlqZ!a&Y25h47Nk#{DGCsVA$kY(S zJ+Gq*nTD!)L}C*RPIj|!)hWobO7HGxX_~hdSy3phY+~=;7pMqSB11j?#05)phJm1NQ) z98M4pC5c3mM8ZkJ;UwWml4!(v?e)lbJug)OZw<1XQ7IL5a|urkrmcOSzXHp)kYtJRNQ}8l-@&3a zr_sD%J|%+ksx5=O$w&Oi`Kgek44&l`J{bE1H{9>Ee8JwyAkkQpD^!%**5Oe5(M zD~LZ>LoA-a&@D#|7>)`^CJmAa16?;!WXZ7s6p1tqr9A)uAOJ~3K~#7$Lx7H@Rs8T< z$L2`F?YqZ0@q(h@|Bss!@P-$oI^;#8{=Gq4R;6%bP?c7ftMKtUZSQrX#*Nz(;^Ndf z!(%Z{eD7^Me(OdSEvNtqd_IL+e|tLXPWU}jA>DBWh3yvXmJ0VN^IJ&hhECGdDRces z@Wdb^gPkOzla7zB53+w-oELuf0_Jc81e7eSyIw#^P~+qyq)1fvHvxk$vJOIJ0uD4xq5X~ zohIE7RHfBzhD@6-Q>ifTeKp$g^F7iZvdQ9zznzk@S_byL!eIBywAR*BS5ZY}{Q}A=TL=WJ zsA*ct(sgHIB;)jVy@1zWhUP29v>Y{%691{a5CT<^P*n-T%osWU^{;DaX$+<-d4<9{ zCvUui!J%lek-;|&{6jJV|E)pmX!OX}DvDThR8pfOZqtlLz!FSoqO>ER%-I&V9*x*6 zOh=GHI-!Wp87I$6mr$MGRz^>6h-BR6?2{MfCR!A$3kYE|Z^*(`>K3NP`W>NVSqu#f zFfkq{5=sybCx}FnghL605O_TbUXMcDfNlE>&N#7|OU_$}&#&S4Yxw;huKd!IJoI>P z{-eLf4HLhq?Vu#U=Nq-wI!(G>Q&@9MQe%M*WoN&avWqTbkygU$q)eGi*~ja&Q!DeR z^}AbluItDD=nFcT_gPAieJ$A+|$$s%PIGlrGHB|lMGLe5I2&vEZD&E!#A?> zsEb&({v3qjXEYoJ$tl^YCyGZ{=PW#ovZ`iED(X;GC2b5QlNnWzDG?`;&>esOoRggd z5z}zu1*T>5yPJ0u`-zKP64d;k5a2(Xw1zT;JA(?V4@)X6Fcku4UVyTE6-vuoJSV@K z4o?}YbeT#;;);5Wnk);Wp)QaifCPqVQD5sPSRygwL=ShL(4+W*RS4UnrZFvC*!=ib z?z-%U_)L?L!u5wTg*UQR{12h+415}1zB*ict=u8r3<{GP*ECikzW0{){> zYb{Z@r%I7luS`m`O929>o{h466&MD#Wg*u$Ap8Mpdi#-eohc34d@}vE%!L=WbM?1R zpr*!`uB#V~=!8QF6vd{#s+@{a&onuA4rxO15EGGc#s@kXAKb-o|8_P%vW17f@CT>o z+Y*ZvKc2-M_!?WVEoab236!DueE25D5w^w1RD!NoM_6{`eD=o&&=ie;FMwQHM_c=G zsG5)dt{2dgF-prDkQIf|{hd^n1kf~v(x8{VecKu7-^xh;Hl{|qh))g?9*;0S6lQXK z1jDe{yJtKt{v#pBQ}B9aM+JBlw)dEv@{UFtYW?_p8a}^<*Q*f-csTp?1tby{+d9Yd zQ@g;K1#}M1^s&TWoB;pUpmkJh(k*4Gw0cESVx?M&|D@AVI#z*cV%zReu4$swH)02a zRQB|uC3RvRSkoNjy&qY}vQ_mt`E%iLf>0=dUz6C|7h~CiB9bku2Ckwgcomhol`Yhi zR59>+hzGy@5b8vfb_?dKKD>?4DTZm8>ghao@6|NFvIwK9#l=Cx~hbl>N0|UAGRTg z$1K93B%!G!gMA_P?U^7Fc7s5!9pLrIcsvRomjrtTElxbD9-m*s=Tq^zHh|Zs(%4YK znWwbz&mUYtO_iTrU6V|Pa&kq>fMyPE_VB?I;L3V+Rij6~)+4iKgRama1sw3(SAq~3 z@ZG?TZ5qh6wMf-9lg%WJ*3~|h1bFzAV ze!)FZWd*1%*5IpcbimJ=fg_x%%qW2%vgRQ$HHji@ z`lgcX-!V?Z(rQW?s;O>Vl$CxaQC8VR-<}sKsi;R*RAQk)w_>-e!YnHTaya1_e4#}8 zSR|ZzZ`(E=ui{1k6*Ntuv(IL2d%0`;dGLC*978ZQ8hG@EMy|MQHIazHrk4h@pUNuW zyS%}*2IU!g{<21oe2pTDHAm|zOHCF3DercAz7W`!l?%RYVsA8N;WuYobV&ZGKfS=k@Ea^l&3*k(}@YXg^I&U#^km(RIqg@j``r}vF-!)t)d}Nu{B}Xwe)|Vzl_Wi|TP6^mp zOeYaHi05sIMCP@s%8ve5WmHun5*Nf17PhPRO~cAD1ns<90{z@M=fA$XKn9fpLqD}QtvSKY-c@?%hW}ljV zoew_lF<(s`Wo=7n6wo1L#`jF}(086=_bV?t-^m`2FR7w_P6xtv25?gM6i=k8emgaP zV{Sd*MBH#Uop{`EjXzx9Uq(}%{Y1oOU{q&pQpd6chH2**f~INZ+C#1f(pVpO(+N-z z3H(T{Dp$yy9+afjZKlk;giYY=iyVVb*9*nI?WA&HXL_Equ(CjHX~lE&akOuG9LtFC zs%mrZmG=|->N}}zEhQE+_`xrC&^r*}rPqd8x1uiB0+ce^;PsdLxb5?QMl&pyr3}8M z9eAtjGChAL@ZIA=2z)hllnWc9YX_sU#QuF@w*C2Ol8L>j9uKx{At4ah$cl%us<|kx z1TajCXv6_OM+zj2^sz+3NRuH2x~6981&@a?y5LY}f{6o4#*Q(M2Z%<1{lfG>)Gk8LAN1JC@$s2h%^Ln~pG zA>&IJJob&J*tV&kFMj70w(fGq*Khmt?yMCzM2-~L{z^Z0eeEISNRoC-Vu|8MTilMX zVa`E=FK}MV9uJZ%rx)InOogQ_O)PAzVcFb9mdPi_M*p8|?^ZlZcB(bPY z#NDS!kR=6DuWegoz<0&Jrpk;?IX!%Df1F?5yq846#564oR~;B;?u5aeT@xI6=FRz; zRolp`@ARe^fm`FkUhbF2e6nX~Mq*e?RUuo1#`2K6iprKS!M z2vR=Kk7k&R7$#db4e)e#n5Zjthel(3>Yr8@7=Jrn8RX7yK8igQX1NLTRX@J=75JN4 zrZxCx0v|~tJ~6`7-dzmX7W)tcn#(x-V~0^$7erR1^g>Tapm|hktIG+L_|nE*B%DmU zv1x-ZoFtku2;CuIO_lJtkw1@D#^+J+cx0Y_Elwov{PB@-o!2|3P-KZY&83u;dDDjA z3$G0E_3yvHr~cz{hDHj;1TH0S6}CfZ)2`Waakcuc#Xk8rH8PFslQQkzGL(0ogx0nWORN$__l=*QWhvSLXyx2dI&%DFkO0#S3J;X2yPrR8FvZ4<&jG=hK_(8oA-0; zzu%7-jk4Twto_oJ>(Hud9Y6m7J>M2t_vnHj8{JQ=Z!g`3PNyUgRemn`%5khecK(cO z0Y#!oBB2CRQwc($IH9Qop{Y1ilg{hOi5OFpaV96@OpHfKCQST31;02H`6v5>sMYlZ`g_q#lc)+y0y5j;Hc0_>wE>Ph+L>9iW6KaX zU2zX$D#3CKT2&vVtJkBIS7Y1O!GjOP$47|ub+gBG!7uT0=6@c|+M`-gRb^W6V=+CQ z+~MpKt^{z7g+hr;@`roO;|6@PeSfbHI&#sRti@@_+GD)9su%s6<+Vs z+0~mo6s!h;e+ItAn^@QGfRVs`y6CoSv8mZBpEs%qk5AVr+1rIu--sM6$ryaXE)e_q z;HPry)zUJAFF@tU2$%*_3M}%;Y#vK8F&bm{H7_AWblPo+rJCP~@YOXs?&l$ZuZJg@ z?A}SQp|c&Z0veZIeI_e6H0CY&mb*ow$@EBGG^)G0Kbc1ar}u~46tG0x$N@ZU19)V- zUWGqC6($nP>D@oXS!cNcXSvw?`4|u5)Mflwej@Ih(Pmu-`?PZ9gmnSshH8a|4M}M8 z1PPpQ8fr@`!Zh63QwIhg+m#rKOnmQd5?fwkhY@FsA{cNJBpOwT4qIZe>c_LB-3jj+ z`ay#akenK4qI)NOhR${gWo zt5=pz9#<*R43nzCL2RENrKS!nGYx*K6iNnscbQa?@*Hf0Wulapqm)UwOmFuXe(@EvQvn{LBkhXh(f2^W9k1Xdi;QWSVg*lC|W8gW+lin=Pmkp#MH z{3VHmGe%+R9K$ekfcNC-ep!*Z_0cKx+*I(d0$cb$YzLhYv^6fe%4A-y^2q0mD#E9l z2G#qA5P=|aMJ3qwG~ioVz1U9s=GZo}&xck~g*G%usiIQ0dc(BV{%pj4+S)gCwj&T_ zUM~IL)96Tv{Vd>ZM7r0fHy8X^3hZ#^vv4To>t`fEEULS}%XmC$dgL!{0D2YvyeZ6{ zK0RN*Urt`}%)fa80PISNy%OSyMvr{)s4O(iFen@7N2;ts@(1(4x80s!1pL%{A&|U& zwAu!IO>U!j0Fx|V;u3{GWE52yO%h2nvz%+10FF3Mg?b%HkUFMkTt3$pNZAIengKf1<1<81&@G8;?io*K0H@OZ zaZ15kZ&?DQNKhh4FO;g%$&)IjKEoi;--}#XiCR*Y1}-0bQ3U)9z&3|!?WbKm3FNvU z@BhKcELu^Eu3OIVYm2E@gL6E0#VmBRAgMRGh_W}MR$WHZP8@uxJi|AuVGR;IEe5EP%A4P?_e6S z@2s7hnTKx+`w*@DVZuFo*k?N6t4*bx_4%V|T~_Um%R9rfv8Y3Uc+4OcH`1V|Ns(m- zqy~Iak|5$5h7mU)B)i~eGJTXBRbV4?;{#Ld?kfoT{D7?G+u!m80N9=oT@9-Ax=)c# z8CNJ#O^eFW5p1s)wW11PnX?1`P_6y`y>uA{+XPVU=aefq(6YFaM8b5EGh$i2KeeKF zJfi|qVm|EwL^7U0YTe&R#5D>d$#l<88+$1dxT+vw!nOBK(mPZT%YO#=AaBBqp1*n^ zXg2MHQssm-LHX_yiQpPbVu|L*yJ{VthL(&Zm(D;)p1 z!)aYomHv%X#cs>8u`Sz4zA!9zzFubHhLgqXByprNd{S;xu9!WAqgg; zHot!`L?~LI-k;?lDtEjk2>@_ZKtAfIGUcIwgnz9mvB2Xaxb{fY%BnX_=V!vZo$NIX zwhDo+NgQ#-I$GvClQ!(E-*O5wigemlDkI4-Qd72!Od59<0TNkKB9Sogcw{tHcJ^uT z9pHr|@!Ylq&u@ztD**ira4NGe1^?D10Khwf($VWml)FkLs@GZ)tsWm`YmPuEuX@wK zAMfg<*U;IDKo6+2U$T_Od6}}cjszfGt=n>oGiK)_7Dxe?2=RC(qbC+~vY<3oM)N3X z0(i8H*zfE!czko5XuRkM-V?wXyn!j)f61o)B`*3zR6Jdx*yqgm$+y3(h+0Wc(EQRS z$`3mhC0O!?z&B$N#=3SeXc%loFlY+8JsLF;oxo%~XJ}PqsQ_CDH~CVy6ELiF(T4PN zZdVGV36L;QRmsidm7UEcfUGEV?KgR4m(Kq2qAJ~Xvv$AqMl*Ncas)^vcIQ;%m3Twe zzVo>oUJ{vW1MKLE@Z68Kvur^LCm&VIyk>t<5~LF^4I9I>@~U`bq(D6G;N|tmPDZaP zrwNcWV9PFp?LD!a>D_s)%k}f0=5M_HMO47CLFwo&#;OehZfCv5u1Y=$STnHCVWaO(Mg?r&A(|K*LqpmfxR62}N!Wy>s9eFRsn zb-+JFvG0s9W{7|gB%>iFdUn%gm~0ni;yZG}R0nM}|lVcSjy zZmQ?&x``w~%y4>m!jy>X5KRb%rvypkfFrV|o2K>6HzQB)ElYqCOXXua0`k29iPDv} zOo!&j)4l?4ZDY|KzC#7y1`vW+Xq?Dk54&`O9YQc=Ig%heLvd6^hGSL*S=gju=$1QS zRbZNeyEerO%C;P!C2&f$e2X_BL-#Fh1DsbOAJgoSZwlgq@A0E9ZpT;OMz&0%wjr1c7gHGUGrLuS^VoQ_LMAnw#VvPq(Y9@T<(1fy zVE%AF=q5r%I=Mt1emr7t9ky7ElW~*NRuPaA?pmO;2+UYO^%ZFJQ<4Q6k3UF7eKYn@AGT?--C{w#D*d@Z<%|2X z-1KFOD`{@2$N--u0os?w;O>3df3f844E`G;!0{#0s+9rdNv}ldN+Gkz<0G(QEm~Ps zUiQzy1$hJl+3U;G`7?v>>;;Ih7bauH-KGL$NY(5EKGKp1WqlNcpxN*+OT{{H6BI9qZax>!LowYP8PV& z$b7pJ;GoOIy{AGxvehTw5|F4_Eo4?`uC?FLd}!c9I=0_$7z~&O6MJ`&jE2&-KvD38 zowGD+YPi|9ZM?n`0%bKU)KnI#66LbQJ4+URRX(sISeLf~^7b-p&%> zpa^hbrF>YeCSOx3F@LQsu}t%#E$P7DH208Gw-XbiOm=s&&q(sJ5WFk|`wX3lp54Sk z<4BTJDE6lTKb3|qY#V=|1b2SLYXiPim5=6LwY<6_4gYH4-QYddCuMdTaoQlIj)7b?~WnPna?{^eN{H4nky zOHPdw-nW}wdXiU#AR&luPguu48|4Lnv&-dqRjTy73bh@I#C%Pqq`n1@FPJ0#i-K=w zz!$Cufe^&?1e1|*x^HMV4`~`dyS-{ z^KJUNlJ?op#&|ye_je|l>hMXo*+?giL7hj~c;XRMj~7kR@^rqtBDcE&NIv+ubpt$# zhLB`}!azbWFqP!B7YArx(?WS=AiY3CO{Je3?#e0Gx*VAAKCk`!mFDgJpAq1^3Z=bH zldca)EL$&RmUz5)m#xI#*m7v#XHC@XGIUl(TI3vx|@3~A*NRNq(@Dp zBgPR;nh^LBF=SP9O1WpbgPBRFIQjB!Sn~QGmU}F}80VV(Hqs4hbZgMXi-Q z_|!gn2gBK_ausj^&RG9@fR#A2dBbnz0U@Q(+EHV!pbjT{@s}ILhQ$IUD zzXUKA~be&giLD**Bm}!6L{;+uJKsUH0Ax2mDdCC;hG2?;;MF@gM9ApJe^JJ=m zI7Mq_1V1GqR9Qh&RBS6r)P|nnD9vrPENE{;mKBg>+U8Vn-R-YWXE)Wk?%}yOYaxCF zSnHaS``$`{qk=eqDG-t*MN%nOmStik;#qn92PgL9V~(}IBbnqCTM!kD^c&)$`@-VM z+1}(0<0A4v*#6_BB@$ar!7kk-8X6@U54pc%=ZO88z)u}BQ!xX#{GaO4kYo)c6(p4t zj-JmSetI5lb1G(Yi%mEybl(mPiBQ(k$<9DR3?DlbFpr_v)x~JuoFe5dy!FKv6Z6 zEW^+~5cojUNHUrTGi*!jHK1j2Jr{oTos^aboDMF@Ajz~ZuH{o7I*gN#X`{N*Pb8`{ zG9H^LB@Dph+6)(vsqgvRHGhB-1J>lr33~r?xRu2x2Di z!;?&Ob<$<%Y_S+G4P$a>#FZc1xOT9evQXkL0+($Ti zz$B7~T)ScNEkgi+^UI}GRhoQ_DzWA;WEQC!r3;qgt*M_;>?hobn%zc{S8Rv~x(0M{ z!6T7b_x$BJ^>B)Ys*h-IVsSExJsxJMOM;5hD!iJse{iM3TPqNBe!1LHu1eSYWmc~g z673!@CG!?#NRU5Kb8MJM?;dvPPR|e7bWWP$(tE@9rh~YzdvR93PPfC?wz2W7wQM|d z9rNZ?(YI@mA76DXeOr52s=^$PLP?;EfacE>u}s?o5HpfY=n=YgLAL=-OPaXyyBE{8 zs6MNg=Yo46X^=}A5Vpowo>IO+B_{>r4ca^k0beP8&6`mLKvZ{n{$4|{&xD30 zO?>gEAEv&k^3VYn%qTU6M#G$a+4Vg4N=`Z7Yk|C*5VjNfQzc9t45O zF=USq)t%`RpBN$5+s$^}vG(JFvA(1@>!FDK!a?5CTAcN9066=!b$t83E^=3HmYuz8 zq?J{dv--FdY`X7ddZ*$@2m(S!k`+{00VEmaAE z=WJ5(V@1!EbBxB!F`1H}k0lFgx#pGvt@rb{uHk0uXNmD0cBp6FL2H#E{nwGZk#h-nGs=DgTHPY8qiX6+^@_xJUSn50lWED~P zc>m_6h#gdYfb9t}HBXbC(j@7~ULhJwB?)C>l-T$P-KNQATM)Au+LII~JR7$UwW#3$ z&K&v*fT7_jo`0pA^Upd0P4g7(rT&rt$DVx@&)oka{gW{?32ZA##7Hu1OKeNRyk&Fw z><=!dWqxB`Zy@R$2M~>MNn*ej zbUHeJ&yXR`dm?JT@&@in;AG;QoI0?ldx)ntb+YlCqw#t@1>zP-Ch-5;JD-;*q5zJ6 zW^m25okgR_%H1{1wN=rITA`>QggW#mbVx$zvIO}LbPT$6>Q;xSTSOMcMi3(XGKi3| zAuKaBGTpV-W_J_a>F}O0?vC41TD!1cc+9*vhj}yK@0<5?zRy$5f$n|0er_-}{f(3b zv$CHbXblj#)Jsz+@OyHzsu}0BY{FNHXkpQ!x9=wX(VUUQ3y3A=r7HkAQ8V3GsSGG^ zC}B|-w_w3SpIfv}N55xYtUTi$%na0r{PbOp&=~SN6S*q6YMwObuZD%f!vTYybJyvQ4rf2p1B5SNX>|kh;iOmf zZ&W$1pj5?|tVhxL%*Yz%tyaFYaL$Dq4?U;4IeX>=%}v{Et#8o+pBnqb@Dow`_8#qI z>yFKT)~YWk4PxtT7OA(mqu zam>7;aQI*w;r&56+8XF+Zy*%(36mzdGq|cM1ruo(Q9i*;&wNFHGlgzU@N{UDTX%Dd7^DT8^igjU^Z{Cee)vbYqfOykMu7+Xy$%S<3ip>_Y6G9kUh3Vhkxc zfz?%}Y<*CLaNl@VWW>J(&Xeo;U(`@aTnr7eq;&#&b#E+huL%b45%3#`pJ)S@~e=HL|UW#Np;U{7){{ji+<+lI;002ovPDHLk FV1iQPf3g4o literal 0 HcmV?d00001 diff --git a/data/gui/gp_remove_track.png b/data/gui/gp_remove_track.png new file mode 100644 index 0000000000000000000000000000000000000000..7e1156279c9e05fe2d48c7c32e24755ee4f21f46 GIT binary patch literal 4028 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 literal 0 HcmV?d00001 diff --git a/data/gui/gp_save.png b/data/gui/gp_save.png new file mode 100644 index 0000000000000000000000000000000000000000..7025c0b4814ee394493634edfb867fe4de8482ec GIT binary patch literal 5618 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 + + + +

+ + \ No newline at end of file diff --git a/data/gui/gpeditor.stkgui b/data/gui/gpeditor.stkgui new file mode 100644 index 000000000..c830525a5 --- /dev/null +++ b/data/gui/gpeditor.stkgui @@ -0,0 +1,45 @@ + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + +
+ +
\ No newline at end of file diff --git a/data/gui/main.stkgui b/data/gui/main.stkgui index 7a297aee9..3d06834b0 100644 --- a/data/gui/main.stkgui +++ b/data/gui/main.stkgui @@ -1,9 +1,9 @@ - +
diff --git a/data/gui/up.png b/data/gui/up.png new file mode 100644 index 0000000000000000000000000000000000000000..ec7f7d30f2a86b467aa05fb85b0a1d222eb2a4b9 GIT binary patch literal 4064 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 SkL%+A0000 >& STKModifiedSpriteBank::getPositions() for (int n=0; n<(int)Rectangles.size(); n++) { - const int h = (int)(Rectangles[n].getHeight()*m_scale); - const int w = (int)(Rectangles[n].getWidth() *m_scale); + const int h = getScaledHeight(Rectangles[n].getHeight()); + const int w = getScaledWidth(Rectangles[n].getWidth()); copy.push_back( core::rect(Rectangles[n].UpperLeftCorner, core::dimension2d(w,h) ) ); @@ -203,8 +204,8 @@ void STKModifiedSpriteBank::draw2DSprite(u32 index, const core::dimension2d& dim = r.getSize(); core::rect dest( pos, - core::dimension2d((int)(dim.Width*m_scale), - (int)(dim.Height*m_scale)) ); + core::dimension2d(getScaledWidth(dim.Width), + getScaledHeight(dim.Height))); if (center) { dest -= dest.getSize() / 2; @@ -295,6 +296,30 @@ void STKModifiedSpriteBank::draw2DSpriteBatch(const core::array& indices, } } // draw2DSpriteBatch +// ---------------------------------------------------------------------------- +void STKModifiedSpriteBank::scaleToHeight(int height) +{ + m_height = height; +} + +// ---------------------------------------------------------------------------- +s32 STKModifiedSpriteBank::getScaledWidth(s32 width) const +{ + if (m_height == 0) + return (s32)((float)width * m_scale); + else + return m_height; +} + +// ---------------------------------------------------------------------------- +s32 STKModifiedSpriteBank::getScaledHeight(s32 height) const +{ + if (m_height == 0) + return (s32)((float)height * m_scale); + else + return m_height; +} + // ---------------------------------------------------------------------------- } // namespace gui } // namespace irr diff --git a/src/guiengine/CGUISpriteBank.h b/src/guiengine/CGUISpriteBank.h index c388d865b..28a87fc3d 100644 --- a/src/guiengine/CGUISpriteBank.h +++ b/src/guiengine/CGUISpriteBank.h @@ -67,12 +67,15 @@ public: m_scale = scale; } + void scaleToHeight(int height); + protected: // this object was getting access after being freed, I wanna see when/why unsigned int m_magic_number; float m_scale; + int m_height; struct SDrawBatch { @@ -91,6 +94,8 @@ protected: IGUIEnvironment* Environment; video::IVideoDriver* Driver; + s32 getScaledWidth(s32 width) const; + s32 getScaledHeight(s32 height) const; }; } // end namespace gui diff --git a/src/guiengine/engine.cpp b/src/guiengine/engine.cpp index a9854516e..f77cb55d1 100644 --- a/src/guiengine/engine.cpp +++ b/src/guiengine/engine.cpp @@ -486,6 +486,14 @@ namespace GUIEngine Used on divs, indicate by how many pixels to pad contents + \n + \subsection prop20 PROP_KEEP_SELECTION + Name in XML files: \c "keep_selection" + + Used on lists, indicates that the list should keep showing the selected item + even when it doesn't have the focus + + \n
\section code Using the engine in code diff --git a/src/guiengine/screen_loader.cpp b/src/guiengine/screen_loader.cpp index 62d426595..e6c5f0c2a 100644 --- a/src/guiengine/screen_loader.cpp +++ b/src/guiengine/screen_loader.cpp @@ -221,6 +221,7 @@ if(prop_name != NULL) widget.m_properties[prop_flag] = core::stringc(prop_name). READ_PROPERTY(max_rows, PROP_MAX_ROWS); READ_PROPERTY(wrap_around, PROP_WRAP_AROUND); READ_PROPERTY(padding, PROP_DIV_PADDING); + READ_PROPERTY(keep_selection, PROP_KEEP_SELECTION); #undef READ_PROPERTY const wchar_t* text = xml->getAttributeValue( L"text" ); diff --git a/src/guiengine/widget.hpp b/src/guiengine/widget.hpp index 874ca87cd..aa3dd5391 100644 --- a/src/guiengine/widget.hpp +++ b/src/guiengine/widget.hpp @@ -103,7 +103,8 @@ namespace GUIEngine PROP_LABELS_LOCATION, PROP_MAX_ROWS, PROP_WRAP_AROUND, - PROP_DIV_PADDING + PROP_DIV_PADDING, + PROP_KEEP_SELECTION, }; bool isWithinATextBox(); diff --git a/src/guiengine/widgets/list_widget.cpp b/src/guiengine/widgets/list_widget.cpp index 13cd2b7f4..d8ed4da81 100644 --- a/src/guiengine/widgets/list_widget.cpp +++ b/src/guiengine/widgets/list_widget.cpp @@ -299,7 +299,8 @@ void ListWidget::unfocused(const int playerID, Widget* new_focus) CGUISTKListBox* list = getIrrlichtElement(); // remove selection when leaving list - if (list != NULL) list->setSelected(-1); + if (list != NULL && m_properties[PROP_KEEP_SELECTION] != "true") + list->setSelected(-1); } // ----------------------------------------------------------------------------- diff --git a/src/io/file_manager.cpp b/src/io/file_manager.cpp index 2ff762ba2..4df7857ad 100644 --- a/src/io/file_manager.cpp +++ b/src/io/file_manager.cpp @@ -188,10 +188,11 @@ FileManager::FileManager() addRootDirs(root_dir+"../../stk-assets"); if ( getenv ( "SUPERTUXKART_ROOT_PATH" ) != NULL ) addRootDirs(getenv("SUPERTUXKART_ROOT_PATH")); - + checkAndCreateConfigDir(); checkAndCreateAddonsDir(); checkAndCreateScreenshotDir(); + checkAndCreateGPDir(); #ifdef WIN32 redirectOutput(); @@ -203,12 +204,14 @@ FileManager::FileManager() for(unsigned int i=0; i -// Copyright (C) 2008-2013 Steve Baker, Joerg Henrichs -// -// This program 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. -// -// This program 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 this program; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - - -#include "io/file_manager.hpp" - -#include "config/user_config.hpp" -#include "graphics/irr_driver.hpp" -#include "graphics/material_manager.hpp" -#include "karts/kart_properties_manager.hpp" -#include "tracks/track_manager.hpp" -#include "utils/command_line.hpp" -#include "utils/log.hpp" -#include "utils/string_utils.hpp" - -#include - -#include -#include -#include -#include -#include -#include - -// For mkdir -#if !defined(WIN32) -# include -# include -# include -# include -#else -# include -# include -# include -# ifndef __CYGWIN__ - /*Needed by the remove directory function */ -# define S_ISDIR(mode) (((mode) & S_IFMT) == S_IFDIR) -# define S_ISREG(mode) (((mode) & S_IFMT) == S_IFREG) -# endif -#endif - - -std::vector FileManager::m_root_dirs; - -#ifdef __APPLE__ -// dynamic data path detection onmac -# include - -bool macSetBundlePathIfRelevant(std::string& data_dir) -{ - Log::debug("FileManager", "Checking whether we are using an app bundle... "); - // the following code will enable STK to find its data when placed in an - // app bundle on mac OS X. - // returns true if path is set, returns false if path was not set - char path[1024]; - CFBundleRef main_bundle = CFBundleGetMainBundle(); assert(main_bundle); - CFURLRef main_bundle_URL = CFBundleCopyBundleURL(main_bundle); - assert(main_bundle_URL); - CFStringRef cf_string_ref = CFURLCopyFileSystemPath(main_bundle_URL, - kCFURLPOSIXPathStyle); - assert(cf_string_ref); - CFStringGetCString(cf_string_ref, path, 1024, kCFStringEncodingASCII); - CFRelease(main_bundle_URL); - CFRelease(cf_string_ref); - - std::string contents = std::string(path) + std::string("/Contents"); - if(contents.find(".app") != std::string::npos) - { - Log::debug("FileManager", "yes\n"); - // executable is inside an app bundle, use app bundle-relative paths - data_dir = contents + std::string("/Resources/"); - return true; - } - else - { - Log::debug("FileManager", "no\n"); - return false; - } -} -#endif - -// ============================================================================ -FileManager* file_manager = 0; - -/** With irrlicht the constructor creates a NULL device. This is necessary to - * handle the Chicken/egg problem with irrlicht: access to the file system - * is given from the device, but we can't create the device before reading - * the user_config file (for resolution, fullscreen). So we create a dummy - * device here to begin with, which is then later (once the real device - * exists) changed in reInit(). - * - */ -FileManager::FileManager() -{ - m_subdir_name.resize(ASSET_COUNT); - m_subdir_name[CHALLENGE ] = "challenges"; - m_subdir_name[FONT ] = "fonts"; - m_subdir_name[GFX ] = "gfx"; - m_subdir_name[GRANDPRIX ] = "grandprix"; - m_subdir_name[GUI ] = "gui"; - m_subdir_name[MODEL ] = "models"; - m_subdir_name[MUSIC ] = "music"; - m_subdir_name[TRANSLATION] = "po"; - m_subdir_name[TEXTURE ] = "textures"; - m_subdir_name[SFX ] = "sfx"; - m_subdir_name[SKIN ] = "skins"; - m_subdir_name[SHADER ] = "shaders"; -#ifdef __APPLE__ - // irrLicht's createDevice method has a nasty habit of messing the CWD. - // since the code above may rely on it, save it to be able to restore - // it after. - char buffer[256]; - getcwd(buffer, 256); -#endif - -#ifdef __APPLE__ - chdir( buffer ); -#endif - - m_file_system = irr_driver->getDevice()->getFileSystem(); - m_file_system->grab(); - - irr::io::path exe_path; - - // Search for the root directory - // ============================= - - // Also check for data dirs relative to the path of the executable. - // This is esp. useful for Visual Studio, since it's not necessary - // to define the working directory when debugging, it works automatically. - std::string root_dir; - if(m_file_system->existFile(CommandLine::getExecName().c_str())) - exe_path = m_file_system->getFileDir(CommandLine::getExecName().c_str()); - if(exe_path.size()==0 || exe_path[exe_path.size()-1]!='/') - exe_path += "/"; - if ( getenv ( "SUPERTUXKART_DATADIR" ) != NULL ) - root_dir = std::string(getenv("SUPERTUXKART_DATADIR"))+"/" ; -#ifdef __APPLE__ - else if( macSetBundlePathIfRelevant( root_dir ) ) { root_dir = root_dir + "data/"; } -#endif - else if(m_file_system->existFile("data")) - root_dir = "data/" ; - else if(m_file_system->existFile("../data")) - root_dir = "../data/" ; - else if(m_file_system->existFile(exe_path+"data")) - root_dir = (exe_path+"data/").c_str(); - else if(m_file_system->existFile(exe_path+"/../data")) - { - root_dir = exe_path.c_str(); - root_dir += "/../data/"; - } - else - { -#ifdef SUPERTUXKART_DATADIR - root_dir = SUPERTUXKART_DATADIR; - if(root_dir.size()==0 || root_dir[root_dir.size()-1]!='/') - root_dir+='/'; - -#else - root_dir = "/usr/local/share/games/supertuxkart/"; -#endif - } - - addRootDirs(root_dir); - if( fileExists(root_dir+"../../stk-assets")) - addRootDirs(root_dir+"../../stk-assets"); - if ( getenv ( "SUPERTUXKART_ROOT_PATH" ) != NULL ) - addRootDirs(getenv("SUPERTUXKART_ROOT_PATH")); - - checkAndCreateConfigDir(); - checkAndCreateAddonsDir(); - checkAndCreateScreenshotDir(); - -#ifdef WIN32 - redirectOutput(); - -#endif - - // We can't use _() here, since translations will only be initalised - // after the filemanager (to get the path to the tranlsations from it) - for(unsigned int i=0; i dir_found; - dir_found.resize(ASSET_COUNT, false); - for(unsigned int i=0; idrop(); -} // dropFileSystem - -//----------------------------------------------------------------------------- -/** This function is used to re-initialise the file-manager after reading in - * the user configuration data. -*/ -void FileManager::reInit() -{ - m_file_system = irr_driver->getDevice()->getFileSystem(); - m_file_system->grab(); - - // Note that we can't push the texture search path in the constructor - // since this also adds a file archive to te file system - and - // m_file_system is deleted (in irr_driver) after - pushTextureSearchPath(m_subdir_name[TEXTURE]); - if(fileExists(m_subdir_name[TEXTURE]+"deprecated/")) - pushTextureSearchPath(m_subdir_name[TEXTURE]+"deprecated/"); - - pushTextureSearchPath(m_subdir_name[GUI]); - - - pushModelSearchPath (m_subdir_name[MODEL]); - pushMusicSearchPath (m_subdir_name[MUSIC]); - - // Add more paths from the STK_MUSIC_PATH environment variable - if(getenv("SUPERTUXKART_MUSIC_PATH")!=NULL) - { - std::string path=getenv("SUPERTUXKART_MUSIC_PATH"); - std::vector dirs = StringUtils::splitPath(path); - for(int i=0;i<(int)dirs.size(); i++) - pushMusicSearchPath(dirs[i]); - } -} // reInit - -//----------------------------------------------------------------------------- -FileManager::~FileManager() -{ - // Clean up left-over files in addons/tmp that are older than 24h - // ============================================================== - // (The 24h delay is useful when debugging a problem with a zip file) - std::set allfiles; - std::string tmp=getAddonsFile("tmp"); - listFiles(allfiles, tmp); - for(std::set::iterator i=allfiles.begin(); - i!=allfiles.end(); i++) - { - if((*i)=="." || (*i)=="..") continue; - // For now there should be only zip files or .part files - // (not fully downloaded files) in tmp. Warn about any - // other files. - std::string full_path=tmp+"/"+*i; - if(StringUtils::getExtension(*i)!="zip" && - StringUtils::getExtension(*i)!="part" ) - { - Log::warn("FileManager", "Unexpected tmp file '%s' found.", - full_path.c_str()); - continue; - } - if(isDirectory(full_path)) - { - // Gee, a .zip file which is a directory - stay away from it - Log::warn("FileManager", "'%s' is a directory and will not be deleted.", - full_path.c_str()); - continue; - } - struct stat mystat; - stat(full_path.c_str(), &mystat); - StkTime::TimeType current = StkTime::getTimeSinceEpoch(); - if(current - mystat.st_ctime <24*3600) - { - if(UserConfigParams::logAddons()) - Log::verbose("FileManager", "'%s' is less than 24h old " - "and will not be deleted.", - full_path.c_str()); - continue; - } - if(UserConfigParams::logAddons()) - Log::verbose("FileManager", "Deleting tmp file'%s'.",full_path.c_str()); - removeFile(full_path); - - } // for i in all files in tmp - - // Clean up rest of file manager - // ============================= - popMusicSearchPath(); - popModelSearchPath(); - popTextureSearchPath(); - popTextureSearchPath(); - m_file_system->drop(); - m_file_system = NULL; -} // ~FileManager - -//----------------------------------------------------------------------------- -/** Adds paths to the list of stk root directories. - * \param roots A ":" separated string of directories to add. - */ -void FileManager::addRootDirs(const std::string &roots) -{ - std::vector all = StringUtils::split(roots, ':'); - for(unsigned int i=0; icreateXMLReader(filename.c_str()); -} // getXMLReader -//----------------------------------------------------------------------------- -/** Reads in a XML file and converts it into a XMLNode tree. - * \param filename Name of the XML file to read. - */ -XMLNode *FileManager::createXMLTree(const std::string &filename) -{ - try - { - XMLNode* node = new XMLNode(filename); - return node; - } - catch (std::runtime_error& e) - { - if (UserConfigParams::logMisc()) - { - Log::error("FileManager", "createXMLTree: %s\n", e.what()); - } - return NULL; - } -} // createXMLTree - -//----------------------------------------------------------------------------- -/** Reads in XML from a string and converts it into a XMLNode tree. - * \param content the string containing the XML content. - */ -XMLNode *FileManager::createXMLTreeFromString(const std::string & content) -{ - try - { - char *b = new char[content.size()]; - memcpy(b, content.c_str(), content.size()); - io::IReadFile * ireadfile = m_file_system->createMemoryReadFile(b, strlen(b), "tempfile", true); - io::IXMLReader * reader = m_file_system->createXMLReader(ireadfile); - XMLNode* node = new XMLNode(reader); - reader->drop(); - return node; - } - catch (std::runtime_error& e) - { - if (UserConfigParams::logMisc()) - { - Log::error("FileManager", "createXMLTreeFromString: %s\n", e.what()); - } - return NULL; - } -} // createXMLTreeFromString - -//----------------------------------------------------------------------------- -/** In order to add and later remove paths we have to specify the absolute - * filename (and replace '\' with '/' on windows). - */ -io::path FileManager::createAbsoluteFilename(const std::string &f) -{ - io::path abs_path=m_file_system->getAbsolutePath(f.c_str()); - abs_path=m_file_system->flattenFilename(abs_path); - return abs_path; -} // createAbsoluteFilename - -//----------------------------------------------------------------------------- -/** Adds a model search path to the list of model search paths. - * This path will be searched before any other existing paths. - */ -void FileManager::pushModelSearchPath(const std::string& path) -{ - m_model_search_path.push_back(path); - const int n=m_file_system->getFileArchiveCount(); - m_file_system->addFileArchive(createAbsoluteFilename(path), - /*ignoreCase*/false, - /*ignorePaths*/false, - io::EFAT_FOLDER); - // A later added file archive should be searched first (so that - // track specific models are found before models in data/models). - // This is not necessary if this is the first member, or if the - // addFileArchive call did not add this file systems (this can - // happen if the file archive has been added prevously, which - // commonly happens since each kart/track specific path is added - // twice: once for textures and once for models). - if(n>0 && (int)m_file_system->getFileArchiveCount()>n) - { - // In this case move the just added file archive - // (which has index n) to position 0 (by -n positions): - m_file_system->moveFileArchive(n, -n); - } -} // pushModelSearchPath - -//----------------------------------------------------------------------------- -/** Adds a texture search path to the list of texture search paths. - * This path will be searched before any other existing paths. - */ -void FileManager::pushTextureSearchPath(const std::string& path) -{ - m_texture_search_path.push_back(path); - const int n=m_file_system->getFileArchiveCount(); - m_file_system->addFileArchive(createAbsoluteFilename(path), - /*ignoreCase*/false, - /*ignorePaths*/false, - io::EFAT_FOLDER); - // A later added file archive should be searched first (so that - // e.g. track specific textures are found before textures in - // data/textures). - // This is not necessary if this is the first member, or if the - // addFileArchive call did not add this file systems (this can - // happen if the file archive has been added previously, which - // commonly happens since each kart/track specific path is added - // twice: once for textures and once for models). - if(n>0 && (int)m_file_system->getFileArchiveCount()>n) - { - // In this case move the just added file archive - // (which has index n) to position 0 (by -n positions): - m_file_system->moveFileArchive(n, -n); - } -} // pushTextureSearchPath - -//----------------------------------------------------------------------------- -/** Removes the last added texture search path from the list of paths. - */ -void FileManager::popTextureSearchPath() -{ - std::string dir = m_texture_search_path.back(); - m_texture_search_path.pop_back(); - m_file_system->removeFileArchive(createAbsoluteFilename(dir)); -} // popTextureSearchPath - -//----------------------------------------------------------------------------- -/** Removes the last added model search path from the list of paths. - */ -void FileManager::popModelSearchPath() -{ - std::string dir = m_model_search_path.back(); - m_model_search_path.pop_back(); - m_file_system->removeFileArchive(createAbsoluteFilename(dir)); -} // popModelSearchPath - -//----------------------------------------------------------------------------- -/** Tries to find the specified file in any of the given search paths. - * \param full_path On return contains the full path of the file, or - * "" if the file is not found. - * \param file_name The name of the file to look for. - * \param search_path The list of paths to search for the file. - * \return True if the file is found, false otherwise. - */ -bool FileManager::findFile(std::string& full_path, - const std::string& file_name, - const std::vector& search_path) const -{ - for(std::vector::const_reverse_iterator - i = search_path.rbegin(); - i != search_path.rend(); ++i) - { - full_path = *i + file_name; - if(m_file_system->existFile(full_path.c_str())) return true; - } - full_path=""; - return false; -} // findFile - -//----------------------------------------------------------------------------- -std::string FileManager::getAssetChecked(FileManager::AssetType type, - const std::string& name, - bool abort_on_error) const -{ - std::string path = m_subdir_name[type]+name; - if(fileExists(path)) - return path; - - if(abort_on_error) - { - Log::fatal("FileManager", "Can not find file '%s' in '%s'", - name.c_str(), m_subdir_name[type].c_str()); - } - return ""; -} // getAssetChecked - -//----------------------------------------------------------------------------- -/** Returns the full path of a file of the given asset class. It is not - * checked if the file actually exists (use getAssetChecked() instead if - * checking is needed). - * \param type Type of the asset class. - * \param name Name of the file to search. - * \return Full path to the file. - */ -std::string FileManager::getAsset(FileManager::AssetType type, - const std::string &name) const -{ - return m_subdir_name[type]+name; -} // getAsset - -//----------------------------------------------------------------------------- -/** Searches in all root directories for the specified file. - * \param name Name of the file to find. - * \return Full path of the file, or "" if not found. - */ -std::string FileManager::getAsset(const std::string &name) const -{ - std::string path; - findFile(path, name, m_root_dirs); - return path; -} // getAsset - -//----------------------------------------------------------------------------- -/** Returns the directory in which screenshots should be stored. - */ -std::string FileManager::getScreenshotDir() const -{ - return m_screenshot_dir; -} // getScreenshotDir - -//----------------------------------------------------------------------------- -/** Returns the full path of a texture file name by searching in all - * directories currently in the texture search path. The difference to - * a call getAsset(TEXTURE,...) is that the latter will only return - * textures from .../textures, while the searchTexture will also - * search e.g. in kart or track directories (depending on what is currently - * being loaded). - * \param file_name Name of the texture file to search. - * \return The full path for the texture, or "" if the texture was not found. - */ -std::string FileManager::searchTexture(const std::string& file_name) const -{ - std::string path; - findFile(path, file_name, m_texture_search_path); - return path; -} // searchTexture - -//----------------------------------------------------------------------------- -/** Returns the list of all directories in which music files are searched. - */ -std::vector FileManager::getMusicDirs() const -{ - return m_music_search_path; -} // getMusicDirs - -//----------------------------------------------------------------------------- -/** If the directory specified in path does not exist, it is created. This - * function does not support recursive operations, so if a directory "a/b" - * is tested, and "a" does not exist, this function will fail. - * \params path Directory to test. - * \return True if the directory exists or could be created, - * false otherwise. - */ -bool FileManager::checkAndCreateDirectory(const std::string &path) -{ - // irrlicht apparently returns true for files and directory - // (using access/_access internally): - if(m_file_system->existFile(io::path(path.c_str()))) - return true; - - Log::info("FileManager", "Creating directory '%s'.", path.c_str()); - - // Otherwise try to create the directory: -#if defined(WIN32) && !defined(__CYGWIN__) - bool error = _mkdir(path.c_str()) != 0; -#else - bool error = mkdir(path.c_str(), 0755) != 0; -#endif - return !error; -} // checkAndCreateDirectory - -//----------------------------------------------------------------------------- -/** If the directory specified in path does not exist, it is created - * recursively (mkdir -p style). - * \params path Directory to test. - * \return True if the directory exists or could be created, false otherwise. - */ -bool FileManager::checkAndCreateDirectoryP(const std::string &path) -{ - // irrlicht apparently returns true for files and directory - // (using access/_access internally): - if(m_file_system->existFile(io::path(path.c_str()))) - return true; - - std::cout << "[FileManager] Creating directory(ies) '" << path << "'.\n"; - - std::vector split = StringUtils::split(path,'/'); - std::string current_path = ""; - for (unsigned int i=0; iexistFile(io::path(current_path.c_str()))) - { - if (!checkAndCreateDirectory(current_path)) - { - Log::error("FileManager", "Can't create dir '%s'", - current_path.c_str()); - break; - } - } - } - bool error = checkAndCreateDirectory(path); - - return error; -} // checkAndCreateDirectory - -//----------------------------------------------------------------------------- -/** Checks if the config directory exists, and it not, tries to create it. - * It will set m_user_config_dir to the path to which user-specific config - * files are stored. - */ -void FileManager::checkAndCreateConfigDir() -{ - if(getenv("SUPERTUXKART_SAVEDIR") && - checkAndCreateDirectory(getenv("SUPERTUXKART_SAVEDIR")) ) - { - m_user_config_dir = getenv("SUPERTUXKART_SAVEDIR"); - } - else - { - -#if defined(WIN32) || defined(__CYGWIN__) - - // Try to use the APPDATA directory to store config files and highscore - // lists. If not defined, used the current directory. - if(getenv("APPDATA")!=NULL) - { - m_user_config_dir = getenv("APPDATA"); - if(!checkAndCreateDirectory(m_user_config_dir)) - { - std::cerr << "[FileManager] Can't create config dir '" - << m_user_config_dir << "', falling back to '.'.\n"; - m_user_config_dir = "."; - } - } - else - m_user_config_dir = "."; - - m_user_config_dir += "/supertuxkart"; - -#elif defined(__APPLE__) - - if (getenv("HOME")!=NULL) - { - m_user_config_dir = getenv("HOME"); - } - else - { - std::cerr << - "[FileManager] No home directory, this should NOT happen!\n"; - // Fall back to system-wide app data (rather than - // user-specific data), but should not happen anyway. - m_user_config_dir = ""; - } - m_user_config_dir += "/Library/Application Support/"; - const std::string CONFIGDIR("SuperTuxKart"); - m_user_config_dir += CONFIGDIR; - -#else - - // Remaining unix variants. Use the new standards for config directory - // i.e. either XDG_CONFIG_HOME or $HOME/.config - if (getenv("XDG_CONFIG_HOME")!=NULL){ - m_user_config_dir = getenv("XDG_CONFIG_HOME"); - } - else if (!getenv("HOME")) - { - std::cerr - << "[FileManager] No home directory, this should NOT happen " - << "- trying '.' for config files!\n"; - m_user_config_dir = "."; - } - else - { - m_user_config_dir = getenv("HOME"); - m_user_config_dir += "/.config"; - if(!checkAndCreateDirectory(m_user_config_dir)) - { - // If $HOME/.config can not be created: - std::cerr << "[FileManager] Cannot create directory '" - << m_user_config_dir <<"', falling back to use '" - << getenv("HOME")<< "'.\n"; - m_user_config_dir = getenv("HOME"); - } - } - m_user_config_dir += "/supertuxkart"; - -#endif - - } // if(getenv("SUPERTUXKART_SAVEDIR") && checkAndCreateDirectory(...)) - - if(m_user_config_dir.size()>0 && *m_user_config_dir.rbegin()!='/') - m_user_config_dir += "/"; - - if(!checkAndCreateDirectory(m_user_config_dir)) - { - Log::warn("FileManager", "Can not create config dir '%s', " - "falling back to '.'.", m_user_config_dir.c_str()); - m_user_config_dir = "./"; - } - return; -} // checkAndCreateConfigDir - -// ---------------------------------------------------------------------------- -/** Creates the directories for the addons data. This will set m_addons_dir - * with the appropriate path, and also create the subdirectories in this - * directory. - */ -void FileManager::checkAndCreateAddonsDir() -{ -#if defined(WIN32) || defined(__CYGWIN__) - m_addons_dir = m_user_config_dir+"addons/"; -#elif defined(__APPLE__) - m_addons_dir = getenv("HOME"); - m_addons_dir += "/Library/Application Support/SuperTuxKart/Addons/"; -#else - m_addons_dir = checkAndCreateLinuxDir("XDG_DATA_HOME", "supertuxkart", - ".local/share", ".stkaddons"); - m_addons_dir += "addons/"; -#endif - - if(!checkAndCreateDirectory(m_addons_dir)) - { - Log::error("FileManager", "Can not create add-ons dir '%s', " - "falling back to '.'.", m_addons_dir.c_str()); - m_addons_dir = "./"; - } - - if (!checkAndCreateDirectory(m_addons_dir + "icons/")) - { - Log::error("FileManager", "Failed to create add-ons icon dir at '%s'.", - (m_addons_dir + "icons/").c_str()); - } - if (!checkAndCreateDirectory(m_addons_dir + "tmp/")) - { - Log::error("FileManager", "Failed to create add-ons tmp dir at '%s'.", - (m_addons_dir + "tmp/").c_str()); - } - -} // checkAndCreateAddonsDir - -// ---------------------------------------------------------------------------- -/** Creates the directories for screenshots. This will set m_screenshot_dir - * with the appropriate path. - */ -void FileManager::checkAndCreateScreenshotDir() -{ -#if defined(WIN32) || defined(__CYGWIN__) - m_screenshot_dir = m_user_config_dir+"screenshots/"; -#elif defined(__APPLE__) - m_screenshot_dir = getenv("HOME"); - m_screenshot_dir += "/Library/Application Support/SuperTuxKart/Screenshots/"; -#else - m_screenshot_dir = checkAndCreateLinuxDir("XDG_CACHE_HOME", "supertuxkart", ".cache/", "."); - m_screenshot_dir += "screenshots/"; -#endif - - if(!checkAndCreateDirectory(m_screenshot_dir)) - { - Log::error("FileManager", "Can not create screenshot directory '%s', " - "falling back to '.'.", m_screenshot_dir.c_str()); - m_screenshot_dir = "."; - } - -} // checkAndCreateScreenshotDir - -// ---------------------------------------------------------------------------- -#if !defined(WIN32) && !defined(__CYGWIN__) && !defined(__APPLE__) - -/** Find a directory to use for remaining unix variants. Use the new standards - * for config directory based on XDG_* environment variables, or a - * subdirectory under $HOME, trying two different fallbacks. It will also - * check if the directory 'dirname' can be created (to avoid problems that - * e.g. $env_name is '/', which exists, but can not be written to. - * \param env_name Name of the environment variable to test first. - * \param dir_name Name of the directory to create - * \param fallback1 Subdirectory under $HOME to use if the environment - * variable is not defined or can not be created. - * \param fallback2 Subdirectory under $HOME to use if the environment - * variable and fallback1 are not defined or can not be created. - */ -std::string FileManager::checkAndCreateLinuxDir(const char *env_name, - const char *dir_name, - const char *fallback1, - const char *fallback2) -{ - bool dir_ok = false; - std::string dir; - - if (getenv(env_name)!=NULL) - { - dir = getenv(env_name); - dir_ok = checkAndCreateDirectory(dir); - if(!dir_ok) - Log::warn("FileManager", "Cannot create $%s.", env_name); - - if(dir[dir.size()-1]!='/') dir += "/"; - // Do an additional test here, e.g. in case that XDG_DATA_HOME is '/' - // and since dir_ok is set, it would not test any of the other options - // like $HOME/.local/share - dir_ok = checkAndCreateDirectory(dir+dir_name); - if(!dir_ok) - Log::warn("FileManager", "Cannot create $%s/%s.", dir.c_str(), - dir_name); - } - - if(!dir_ok && getenv("HOME")) - { - // Use ~/.local/share : - dir = getenv("HOME"); - if(dir.size()>0 && dir[dir.size()-1]!='/') dir += "/"; - dir += fallback1; - // This will create each individual subdirectory if - // dir_name contains "/". - dir_ok = checkAndCreateDirectoryP(dir); - if(!dir_ok) - Log::warn("FileManager", "Cannot create $HOME/%s.", - fallback1); - } - if(!dir_ok && fallback2 && getenv("HOME")) - { - dir = getenv("HOME"); - if(dir.size()>0 && dir[dir.size()-1]!='/') dir += "/"; - dir += fallback2; - dir_ok = checkAndCreateDirectory(dir); - if(!dir_ok) - Log::warn("FileManager", "Cannot create $HOME/%s.", - fallback2); - } - - if(!dir_ok) - { - Log::warn("FileManager", "Falling back to use '.'."); - dir = "./"; - } - - if(dir.size()>0 && dir[dir.size()-1]!='/') dir += "/"; - dir += dir_name; - dir_ok = checkAndCreateDirectory(dir); - if(!dir_ok) - { - // If the directory can not be created - Log::error("FileManager", "Cannot create directory '%s', " - "falling back to use '.'.", dir.c_str()); - dir="./"; - } - if(dir.size()>0 && dir[dir.size()-1]!='/') dir += "/"; - return dir; -} // checkAndCreateLinuxDir -#endif - -//----------------------------------------------------------------------------- -/** Redirects output to go into files in the user's config directory - * instead of to the console. - */ -void FileManager::redirectOutput() -{ - //Enable logging of stdout and stderr to logfile - std::string logoutfile = getUserConfigFile("stdout.log"); - Log::verbose("main", "Error messages and other text output will " - "be logged to %s.", logoutfile.c_str()); - Log::openOutputFiles(logoutfile); -} // redirectOutput - -//----------------------------------------------------------------------------- -/** Returns the directory for addon files. */ -const std::string &FileManager::getAddonsDir() const -{ - return m_addons_dir; -} // getAddonsDir - -//----------------------------------------------------------------------------- -/** Returns the full path of a file in the addons directory. - * \param name Name of the file. - */ -std::string FileManager::getAddonsFile(const std::string &name) -{ - return getAddonsDir()+name; -} // getAddonsFile - -//----------------------------------------------------------------------------- -/** Returns the full path of the config directory. - */ -std::string FileManager::getUserConfigFile(const std::string &fname) const -{ - return m_user_config_dir+fname; -} // getUserConfigFile - -//----------------------------------------------------------------------------- -/** Returns the full path of a music file by searching all music search paths. - * It throws an exception if the file is not found. - * \param file_name File name to search for. - */ -std::string FileManager::searchMusic(const std::string& file_name) const -{ - std::string path; - bool success = findFile(path, file_name, m_music_search_path); - if(!success) - { - // If a music file is not found in any of the music search paths - // check all root dirs. This is used by stk_config to load the - // title music before any music search path is defined) - path = getAsset(MUSIC, file_name); - success = fileExists(path); - } - if (!success) - { - throw std::runtime_error( - "[FileManager::getMusicFile] Cannot find music file '" - +file_name+"'."); - } - return path; -} // searchMusic - -//----------------------------------------------------------------------------- -/** Returns true if the given name is a directory. - * \param path File name to test. - */ -bool FileManager::isDirectory(const std::string &path) const -{ - struct stat mystat; - std::string s(path); - // At least on windows stat returns an error if there is - // a '/' at the end of the path. - if(s[s.size()-1]=='/') - s.erase(s.end()-1, s.end()); - if(stat(s.c_str(), &mystat) < 0) return false; - return S_ISDIR(mystat.st_mode); -} // isDirectory - -//----------------------------------------------------------------------------- -/** Returns a list of files in a given directory. - * \param result A reference to a std::vector which will - * hold all files in a directory. The vector will be cleared. - * \param dir The director for which to get the directory listing. - * \param make_full_path If set to true, all listed files will be full paths. - */ -void FileManager::listFiles(std::set& result, - const std::string& dir, - bool make_full_path) const -{ - result.clear(); - -#ifndef ANDROID - if(!isDirectory(dir)) - return; -#endif - - io::path previous_cwd = m_file_system->getWorkingDirectory(); - - if(!m_file_system->changeWorkingDirectoryTo( dir.c_str() )) - { - Log::error("FileManager", "listFiles : Could not change CWD!\n"); - return; - } - irr::io::IFileList* files = m_file_system->createFileList(); - - for(int n=0; n<(int)files->getFileCount(); n++) - { - result.insert(make_full_path ? dir+"/"+ files->getFileName(n).c_str() - : files->getFileName(n).c_str() ); - } - - m_file_system->changeWorkingDirectoryTo( previous_cwd ); - files->drop(); -} // listFiles - -//----------------------------------------------------------------------------- -/** Creates a directory for an addon. - * \param addons_name Name of the directory to create. - * \param addons_type The type, which is used as a subdirectory. E.g.: - * 'karts' (m_addons_dir/karts/name will be created). - */ -void FileManager::checkAndCreateDirForAddons(const std::string &dir) -{ - // Tries to create directory recursively - bool success = checkAndCreateDirectoryP(dir); - if(!success) - { - Log::warn("FileManager", "There is a problem with the addons dir."); - return; - } -} // checkAndCreateDirForAddons - -// ---------------------------------------------------------------------------- -/** Removes the specified file, returns true if successful, or false - * if the file is not a regular file or can not be removed. - */ -bool FileManager::removeFile(const std::string &name) const -{ - struct stat mystat; - if(stat(name.c_str(), &mystat) < 0) return false; - if( S_ISREG(mystat.st_mode)) - return remove(name.c_str())==0; - return false; -} // removeFile - -// ---------------------------------------------------------------------------- -/** Removes a directory (including all files contained). The function could - * easily recursively delete further subdirectories, but this is commented - * out atm (to limit the amount of damage in case of a bug). - * \param name Directory name to remove. - * \param return True if removal was successful. - */ -bool FileManager::removeDirectory(const std::string &name) const -{ - std::set files; - listFiles(files, name, /*is full path*/ true); - for(std::set::iterator i=files.begin(); i!=files.end(); i++) - { - if((*i)=="." || (*i)=="..") continue; - if(UserConfigParams::logMisc()) - Log::verbose("FileManager", "Deleting directory '%s'.", - (*i).c_str()); - if(isDirectory(*i)) - { - // This should not be necessary (since this function is only - // used to remove addons), and it limits the damage in case - // of any bugs - i.e. if name should be "/" or so. - // removeDirectory(full_path); - } - else - { - removeFile(*i); - } - } -#if defined(WIN32) - return RemoveDirectory(name.c_str())==TRUE; -#else - return remove(name.c_str())==0; -#endif -} // remove directory - diff --git a/src/io/file_manager.hpp b/src/io/file_manager.hpp index 4037cb464..abeba8084 100644 --- a/src/io/file_manager.hpp +++ b/src/io/file_manager.hpp @@ -46,10 +46,10 @@ class FileManager : public NoCopy public: /** The various asset types (and directories) STK might request. * The last entry ASSET_COUNT specifies the number of entries. */ - enum AssetType {ASSET_MIN, - CHALLENGE=ASSET_MIN, + enum AssetType {ASSET_MIN, + CHALLENGE=ASSET_MIN, FONT, GFX, GRANDPRIX, GUI, MODEL, MUSIC, - SFX, SHADER, SKIN, TEXTURE, TRANSLATION, + SFX, SHADER, SKIN, TEXTURE, TRANSLATION, ASSET_MAX = TRANSLATION, ASSET_COUNT}; private: @@ -72,6 +72,9 @@ private: /** Directory to store screenshots in. */ std::string m_screenshot_dir; + /** Directory where user-defined grand prix are stored. */ + std::string m_gp_dir; + std::vector m_texture_search_path, m_model_search_path, @@ -88,6 +91,7 @@ private: bool isDirectory(const std::string &path) const; void checkAndCreateAddonsDir(); void checkAndCreateScreenshotDir(); + void checkAndCreateGPDir(); #if !defined(WIN32) && !defined(__CYGWIN__) && !defined(__APPLE__) std::string checkAndCreateLinuxDir(const char *env_name, const char *dir_name, @@ -106,6 +110,7 @@ public: XMLNode *createXMLTreeFromString(const std::string & content); std::string getScreenshotDir() const; + std::string getGPDir() const; bool checkAndCreateDirectoryP(const std::string &path); const std::string &getAddonsDir() const; std::string getAddonsFile(const std::string &name); diff --git a/src/race/grand_prix_data.cpp b/src/race/grand_prix_data.cpp index 2a62811eb..23cfe6b1a 100644 --- a/src/race/grand_prix_data.cpp +++ b/src/race/grand_prix_data.cpp @@ -23,37 +23,62 @@ #include "challenges/unlock_manager.hpp" #include "config/player_manager.hpp" #include "io/file_manager.hpp" +#include "io/utf_writer.hpp" #include "tracks/track_manager.hpp" #include "tracks/track.hpp" #include "utils/string_utils.hpp" -#include "utils/translation.hpp" #include +#include +#include #include -GrandPrixData::GrandPrixData(const std::string filename) -{ - load_from_file(file_manager->getAsset(FileManager::GRANDPRIX, filename), - filename); -} + // ---------------------------------------------------------------------------- -GrandPrixData::GrandPrixData(const std::string dir, const std::string filename) -{ - assert(dir[dir.size() - 1] == '/'); - load_from_file(dir + filename, filename); -} -// ---------------------------------------------------------------------------- -void GrandPrixData::load_from_file(const std::string fullpath, - const std::string filename) +GrandPrixData::GrandPrixData(const std::string& filename) throw(std::logic_error) { m_filename = filename; m_id = StringUtils::getBasename(StringUtils::removeExtension(filename)); + m_editable = (filename.find(file_manager->getGPDir(), 0) == 0); + reload(); +} - XMLNode * root = file_manager->createXMLTree(fullpath); - if (!root) +// ---------------------------------------------------------------------------- +void GrandPrixData::setId(const std::string& id) +{ + m_id = id; +} + +// ---------------------------------------------------------------------------- +void GrandPrixData::setName(const irr::core::stringw& name) +{ + m_name = name; +} + +// ---------------------------------------------------------------------------- +void GrandPrixData::setFilename(const std::string& filename) +{ + m_filename = filename; +} + +// ---------------------------------------------------------------------------- +void GrandPrixData::setEditable(const bool editable) +{ + m_editable = editable; +} + +// ---------------------------------------------------------------------------- +void GrandPrixData::reload() +{ + m_tracks.clear(); + m_laps.clear(); + m_reversed.clear(); + + std::auto_ptr root(file_manager->createXMLTree(m_filename)); + if (root.get() == NULL) { - Log::error("GrandPrixData","Error while trying to read grandprix file " - "'%s'", fullpath.c_str()); + Log::error("GrandPrixData","Error while trying to read grandprix file '%s'", + m_filename.c_str()); throw std::logic_error("File not found"); } @@ -61,24 +86,18 @@ void GrandPrixData::load_from_file(const std::string fullpath, if (root->getName() == "supertuxkart_grand_prix") { - std::string temp_name; - if (root->get("name", &temp_name) == 0) + if (root->get("name", &m_name) == 0) { - Log::error("GrandPrixData", "Error while trying to read grandprix " - "file '%s' : missing 'name' attribute\n", - fullpath.c_str()); - delete root; + Log::error("GrandPrixData", "Error while trying to read grandprix file '%s' : " + "missing 'name' attribute\n", m_filename.c_str()); throw std::logic_error("File contents are incomplete or corrupt"); } - m_name = temp_name.c_str(); foundName = true; } else { - Log::error("GrandPrixData", "Error while trying to read grandprix file " - "'%s' : Root node has an unexpected name\n", - fullpath.c_str()); - delete root; + Log::error("GrandPrixData", "Error while trying to read grandprix file '%s' : " + "Root node has an unexpected name\n", m_filename.c_str()); throw std::logic_error("File contents are incomplete or corrupt"); } @@ -95,20 +114,17 @@ void GrandPrixData::load_from_file(const std::string fullpath, int numLaps; bool reversed = false; - const int idFound = node->get("id", &trackID); - const int lapFound = node->get("laps", &numLaps); + const int idFound = node->get("id", &trackID ); + const int lapFound = node->get("laps", &numLaps ); // Will stay false if not found node->get("reverse", &reversed ); if (!idFound || !lapFound) { - Log::error("GrandPrixData", "Error while trying to read " - "grandprix file '%s' : tag does not have " - "idi and laps reverse attributes. \n", - fullpath.c_str()); - delete root; - throw std::logic_error("File contents are incomplete or " - "corrupt"); + Log::error("GrandPrixData", "Error while trying to read grandprix file '%s' : " + " tag does not have idi and laps reverse attributes. \n", + m_filename.c_str()); + throw std::logic_error("File contents are incomplete or corrupt"); } // Make sure the track really is reversible @@ -127,27 +143,58 @@ void GrandPrixData::load_from_file(const std::string fullpath, } else { - Log::error("Unknown node in Grand Prix XML file: %s/n", - node->getName().c_str()); - delete root; + std::cerr << "Unknown node in Grand Prix XML file : " << node->getName().c_str() << std::endl; throw std::runtime_error("Unknown node in sfx XML file"); } - }// end for - - delete root; + }// nend for // sanity checks if (!foundName) { - Log::error("GrandPrixData", "Error while trying to read grandprix file " - "'%s' : missing 'name' attribute\n", fullpath.c_str()); + Log::error("GrandPrixData", "Error while trying to read grandprix file '%s' : " + "missing 'name' attribute\n", m_filename.c_str()); throw std::logic_error("File contents are incomplete or corrupt"); } } + +// ---------------------------------------------------------------------------- +bool GrandPrixData::writeToFile() +{ + try + { + UTFWriter file(m_filename.c_str()); + if (file.is_open()) + { + file << L"\n\n\n"; + for (unsigned int i = 0; i < getNumberOfTracks(); i++) + { + file << + L"\t\n"; + } + file << L"\n\n"; + + file.close(); + + return true; + } + + return false; + } + catch (std::runtime_error& e) + { + Log::error("GrandPrixData", "Failed to write '%s'; cause: %s\n", + m_filename.c_str(), e.what()); + return false; + } +} + // ---------------------------------------------------------------------------- bool GrandPrixData::checkConsistency(bool chatty) const { - for(unsigned int i=0; igetTrack(m_tracks[i]); @@ -166,6 +213,7 @@ bool GrandPrixData::checkConsistency(bool chatty) const return true; } // checkConsistency + // ---------------------------------------------------------------------------- /** Returns true if the track is available. This is used to test if Fort Magma * is available (this way FortMagma is not used in the last Grand Prix in @@ -182,7 +230,7 @@ bool GrandPrixData::isTrackAvailable(const std::string &id) const void GrandPrixData::getLaps(std::vector *laps) const { laps->clear(); - for(unsigned int i=0; i< m_tracks.size(); i++) + for(unsigned int i=0; i< getNumberOfTracks(); i++) if(isTrackAvailable(m_tracks[i])) laps->push_back(m_laps[i]); } // getLaps @@ -191,16 +239,129 @@ void GrandPrixData::getLaps(std::vector *laps) const void GrandPrixData::getReverse(std::vector *reverse) const { reverse->clear(); - for(unsigned int i=0; i< m_tracks.size(); i++) + for(unsigned int i=0; i< getNumberOfTracks(); i++) if(isTrackAvailable(m_tracks[i])) reverse->push_back(m_reversed[i]); } // getReverse +// ---------------------------------------------------------------------------- +bool GrandPrixData::isEditable() const +{ + return m_editable; +} // isEditable + +// ---------------------------------------------------------------------------- +unsigned int GrandPrixData::getNumberOfTracks() const +{ + return m_tracks.size(); +} + +// ---------------------------------------------------------------------------- +irr::core::stringw GrandPrixData::getTrackName(const unsigned int track) const +{ + assert(track < getNumberOfTracks()); + Track* t = track_manager->getTrack(m_tracks[track]); + assert(t != NULL); + return t->getName(); +} + +// ---------------------------------------------------------------------------- +const std::string& GrandPrixData::getTrackId(const unsigned int track) const +{ + assert(track < getNumberOfTracks()); + return m_tracks[track]; +} + +// ---------------------------------------------------------------------------- +unsigned int GrandPrixData::getLaps(const unsigned int track) const +{ + assert(track < getNumberOfTracks()); + return m_laps[track]; +} + +// ---------------------------------------------------------------------------- +bool GrandPrixData::getReverse(const unsigned int track) const +{ + assert(track < getNumberOfTracks()); + return m_reversed[track]; +} + +// ---------------------------------------------------------------------------- +void GrandPrixData::moveUp(const unsigned int track) +{ + assert (track > 0 && track < getNumberOfTracks()); + + std::swap(m_tracks[track], m_tracks[track - 1]); + std::swap(m_laps[track], m_laps[track - 1]); + m_reversed.swap(m_reversed[track], m_reversed[track - 1]); +} + +// ---------------------------------------------------------------------------- +void GrandPrixData::moveDown(const unsigned int track) +{ + assert (track < (getNumberOfTracks() - 1)); + + std::swap(m_tracks[track], m_tracks[track + 1]); + std::swap(m_laps[track], m_laps[track + 1]); + m_reversed.swap(m_reversed[track], m_reversed[track + 1]); +} + +// ---------------------------------------------------------------------------- +void GrandPrixData::addTrack(Track* track, unsigned int laps, bool reverse, + int position) +{ + int n; + + n = getNumberOfTracks(); + assert (track != NULL); + assert (laps > 0); + assert (position >= -1 && position < n); + + if (position < 0 || position == (n - 1) || m_tracks.empty()) + { + //Append new track to the end of the list + m_tracks.push_back(track->getIdent()); + m_laps.push_back(laps); + m_reversed.push_back(reverse); + } + else + { + //Insert new track right after the specified position. Caution: + //std::vector inserts elements _before_ the specified position + m_tracks.insert(m_tracks.begin() + position + 1, track->getIdent()); + m_laps.insert(m_laps.begin() + position + 1, laps); + m_reversed.insert(m_reversed.begin() + position + 1, reverse); + } +} + +// ---------------------------------------------------------------------------- +void GrandPrixData::editTrack(unsigned int t, Track* track, + unsigned int laps, bool reverse) +{ + assert (t < getNumberOfTracks()); + assert (track != NULL); + assert (laps > 0); + + m_tracks[t] = track->getIdent(); + m_laps[t] = laps; + m_reversed[t] = reverse; +} + +// ---------------------------------------------------------------------------- +void GrandPrixData::remove(const unsigned int track) +{ + assert (track < getNumberOfTracks()); + + m_tracks.erase(m_tracks.begin() + track); + m_laps.erase(m_laps.begin() + track); + m_reversed.erase(m_reversed.begin() + track); +} + // ---------------------------------------------------------------------------- const std::vector& GrandPrixData::getTrackNames() const { m_really_available_tracks.clear(); - for(unsigned int i=0; i< m_tracks.size(); i++) + for(unsigned int i=0; i< getNumberOfTracks(); i++) { if(isTrackAvailable(m_tracks[i])) m_really_available_tracks.push_back(m_tracks[i]); diff --git a/src/race/grand_prix_data.hpp b/src/race/grand_prix_data.hpp index 8061d88c9..6ecff2e98 100644 --- a/src/race/grand_prix_data.hpp +++ b/src/race/grand_prix_data.hpp @@ -27,7 +27,8 @@ #include #include "utils/translation.hpp" -#include "io/file_manager.hpp" + +class Track; /** Simple class that hold the data relevant to a 'grand_prix', aka. a number * of races that has to be completed one after the other @@ -65,28 +66,49 @@ private: /** Whether the track in question should be done in reverse mode */ std::vector m_reversed; - void load_from_file(const std::string fullpath, const std::string filename); + /** Wether the user can edit this grand prix or not */ + bool m_editable; + bool isTrackAvailable(const std::string &id) const; + public: /** Load the GrandPrixData from the given filename */ #if (defined(WIN32) || defined(_WIN32)) && !defined(__MINGW32__) #pragma warning(disable:4290) #endif - GrandPrixData () {}; // empty for initialising - GrandPrixData(const std::string filename); - GrandPrixData (const std::string dir, const std::string filename); + GrandPrixData (const std::string& filename) throw(std::logic_error); + GrandPrixData () {}; // empty for initialising + void setId(const std::string& id); + void setName(const irr::core::stringw& name); + void setFilename(const std::string& filename); + void setEditable(const bool editable); + void reload(); + bool writeToFile(); - bool checkConsistency(bool chatty=true) const; + bool checkConsistency(bool chatty=true) const; const std::vector& getTrackNames() const; - void getLaps(std::vector *laps) const; - void getReverse(std::vector *reverse) const; + void getLaps(std::vector *laps) const; + void getReverse(std::vector *reverse) const; + bool isEditable() const; + unsigned int getNumberOfTracks() const; + const std::string& getTrackId(const unsigned int track) const; + irr::core::stringw getTrackName(const unsigned int track) const; + unsigned int getLaps(const unsigned int track) const; + bool getReverse(const unsigned int track) const; + void moveUp(const unsigned int track); + void moveDown(const unsigned int track); + void addTrack(Track* track, unsigned int laps, + bool reverse, int position=-1); + void editTrack(unsigned int t, Track* track, + unsigned int laps, bool reverse); + void remove(const unsigned int track); // ------------------------------------------------------------------------ /** @return the (potentially translated) user-visible name of the Grand * Prix (apply fribidi as needed) */ - const irr::core::stringw getName() const { return _LTR(m_name.c_str()); } + irr::core::stringw getName() const { return _LTR(m_name.c_str()); } // ------------------------------------------------------------------------ /** @return the internal name identifier of the Grand Prix (not translated) */ diff --git a/src/race/grand_prix_manager.cpp b/src/race/grand_prix_manager.cpp index 508c35eae..653933224 100644 --- a/src/race/grand_prix_manager.cpp +++ b/src/race/grand_prix_manager.cpp @@ -18,66 +18,126 @@ #include "race/grand_prix_manager.hpp" -#include +#include "config/user_config.hpp" +#include "grand_prix_data.hpp" #include "io/file_manager.hpp" #include "utils/string_utils.hpp" -#include "config/user_config.hpp" + +#include +#include +#include GrandPrixManager *grand_prix_manager = NULL; +const char* GrandPrixManager::SUFFIX = ".grandprix"; + +// ---------------------------------------------------------------------------- +void GrandPrixManager::loadFiles() +{ + std::set dirs; + + //Add all the directories to a set to avoid duplicates + dirs.insert(file_manager->getAsset(FileManager::GRANDPRIX, "")); + dirs.insert(file_manager->getGPDir()); + dirs.insert(UserConfigParams::m_additional_gp_directory); + + for (std::set::const_iterator it = dirs.begin(); it != dirs.end(); ++it) + { + std::string dir = *it; + if (!dir.empty() && dir[dir.size() - 1] == '/') + loadDir(dir); + } +} + +// ---------------------------------------------------------------------------- +void GrandPrixManager::loadDir(const std::string& dir) +{ + Log::info("GrandPrixManager", "Loading Grand Prix files from %s", dir.c_str()); + assert(!dir.empty() && dir[dir.size() - 1] == '/'); + + // Findout which grand prixs are available and load them + std::set result; + file_manager->listFiles(result, dir); + for(std::set::iterator i = result.begin(); i != result.end(); i++) + { + if (StringUtils::hasSuffix(*i, SUFFIX)) + load(dir + *i); + } // for i +} // loadDir + +// ---------------------------------------------------------------------------- +void GrandPrixManager::load(const std::string& filename) +{ + GrandPrixData* gp; + + try + { + gp = new GrandPrixData(filename); + m_gp_data.push_back(gp); + Log::debug("GrandPrixManager", "Grand Prix '%s' loaded from %s", + gp->getId().c_str(), filename.c_str()); + } + catch (std::logic_error& er) + { + Log::error("GrandPrixManager", "Ignoring GP %s (%s)\n", + filename.c_str(), er.what()); + } +} // load + +// ---------------------------------------------------------------------------- +void GrandPrixManager::reload() +{ + for(unsigned int i=0; igetId() == id); + + return exists; +} + +// ---------------------------------------------------------------------------- +bool GrandPrixManager::existsName(const irr::core::stringw& name) const +{ + bool exists; + + exists = false; + for (unsigned int i = 0; !exists && i < m_gp_data.size(); i++) + exists = (m_gp_data[i]->getName() == name); + + return exists; +} + +// ---------------------------------------------------------------------------- GrandPrixManager::GrandPrixManager() { - // Findout which grand prixs are available and load them - // Grand Prix in the standart directory - std::set result; - std::string gp_dir = file_manager->getAsset(FileManager::GRANDPRIX, ""); - file_manager->listFiles(result, gp_dir); - for(std::set::iterator i = result.begin(); - i != result.end() ; i++) - { - if (StringUtils::hasSuffix(*i, ".grandprix")) - { - try - { - m_gp_data.push_back(new GrandPrixData(*i)); - Log::debug("GrandPrixManager", "Grand Prix %s loaded.", - i->c_str()); - } - catch (std::logic_error& e) - { - Log::error("GrandPrixManager", "Ignoring GP %s ( %s ) \n", - i->c_str(), e.what()); - } - } - } - - // Load additional Grand Prix - const std::string dir = UserConfigParams::m_additional_gp_directory; - if(dir != "") { - Log::info("GrandPrixManager", "Loading additional Grand Prix from " - "%s ...", dir.c_str()); - file_manager->listFiles(result, dir); - for(std::set::iterator i = result.begin(); - i != result.end() ; i++) - { - if (StringUtils::hasSuffix(*i, ".grandprix")) - { - try - { - m_gp_data.push_back(new GrandPrixData(dir, *i)); - Log::debug("GrandPrixManager", "Grand Prix %s loaded from " - "%s", i->c_str(), - dir.c_str()); - } - catch (std::logic_error& e) - { - Log::error("GrandPrixManager", "Ignoring GP %s ( %s ) \n", - i->c_str(), e.what()); - } - } - } // end for - } // end if + loadFiles(); } // GrandPrixManager + // ---------------------------------------------------------------------------- GrandPrixManager::~GrandPrixManager() { @@ -87,15 +147,21 @@ GrandPrixManager::~GrandPrixManager() } // for i } // ~GrandPrixManager + // ---------------------------------------------------------------------------- const GrandPrixData* GrandPrixManager::getGrandPrix(const std::string& s) const { - for(unsigned int i=0; igetId() == s) - return m_gp_data[i]; - - return NULL; + return editGrandPrix(s); } // getGrandPrix + +// ---------------------------------------------------------------------------- +GrandPrixData* GrandPrixManager::editGrandPrix(const std::string& s) const +{ + for(unsigned int i=0; igetId()==s) return m_gp_data[i]; + return NULL; +} + // ---------------------------------------------------------------------------- void GrandPrixManager::checkConsistency() { @@ -104,9 +170,64 @@ void GrandPrixManager::checkConsistency() if(!m_gp_data[i]->checkConsistency()) { // delete this GP, since a track is missing - m_gp_data.erase(m_gp_data.begin()+i); + delete *(m_gp_data.erase(m_gp_data.begin()+i)); i--; } } } // checkConsistency + // ---------------------------------------------------------------------------- +GrandPrixData* GrandPrixManager::createNew(const irr::core::stringw& newName) +{ + if (existsName(newName)) + return NULL; + + std::string newID = generateId(); + + GrandPrixData* gp = new GrandPrixData; + gp->setId(newID); + gp->setName(newName); + gp->setFilename(file_manager->getGPDir() + newID + SUFFIX); + gp->setEditable(true); + gp->writeToFile(); + m_gp_data.push_back(gp); + + return gp; +} + +// ---------------------------------------------------------------------------- +GrandPrixData* GrandPrixManager::copy(const std::string& id, + const irr::core::stringw& newName) +{ + if (existsName(newName)) + return NULL; + + std::string newID = generateId(); + + GrandPrixData* gp = new GrandPrixData(*getGrandPrix(id)); + gp->setId(newID); + gp->setName(newName); + gp->setFilename(file_manager->getGPDir() + newID + SUFFIX); + gp->setEditable(true); + gp->writeToFile(); + m_gp_data.push_back(gp); + + return gp; +} + +// ---------------------------------------------------------------------------- +void GrandPrixManager::remove(const std::string& id) +{ + const GrandPrixData* gp = getGrandPrix(id); + assert(gp != NULL); + + if (gp->isEditable()) + { + file_manager->removeFile(gp->getFilename()); + reload(); + } + else + { + Log::warn("GrandPrixManager", "Grand Prix '%s' cannot be removed\n", gp->getId().c_str()); + } +} diff --git a/src/race/grand_prix_manager.hpp b/src/race/grand_prix_manager.hpp index 2afb979b6..8238fc6de 100644 --- a/src/race/grand_prix_manager.hpp +++ b/src/race/grand_prix_manager.hpp @@ -30,16 +30,31 @@ class GrandPrixManager { private: + static const char* SUFFIX; + + void loadFiles(); + void loadDir(const std::string& dir); + void load(const std::string &filename); + + std::string generateId(); + + bool existsId(const std::string& id) const; + bool existsName(const irr::core::stringw& name) const; + std::vector m_gp_data; public: GrandPrixManager(); ~GrandPrixManager(); - void load(const std::string &filename); - const GrandPrixData* getGrandPrix(int i) const { return m_gp_data[i]; } + void reload(); + const GrandPrixData* getGrandPrix(const int i) const { return m_gp_data[i]; } const GrandPrixData* getGrandPrix(const std::string& s) const; + GrandPrixData* editGrandPrix(const std::string& s) const; unsigned int getNumberOfGrandPrix() const { return m_gp_data.size(); } void checkConsistency(); + GrandPrixData* createNew(const irr::core::stringw& newName); + GrandPrixData* copy(const std::string& id, const irr::core::stringw& newName); + void remove(const std::string& id); }; // GrandPrixManager extern GrandPrixManager *grand_prix_manager; diff --git a/src/states_screens/dialogs/enter_gp_name_dialog.cpp b/src/states_screens/dialogs/enter_gp_name_dialog.cpp new file mode 100644 index 000000000..b8b9d566f --- /dev/null +++ b/src/states_screens/dialogs/enter_gp_name_dialog.cpp @@ -0,0 +1,137 @@ +// SuperTuxKart - a fun racing game with go-kart +// Copyright (C) 2014 Marc Coll +// +// This program 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. +// +// This program 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 this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +#include "states_screens/dialogs/enter_gp_name_dialog.hpp" + +#include "audio/sfx_manager.hpp" +#include "guiengine/engine.hpp" +#include "guiengine/widgets/button_widget.hpp" +#include "guiengine/widgets/label_widget.hpp" +#include "guiengine/widgets/text_box_widget.hpp" +#include "race/grand_prix_manager.hpp" +#include "states_screens/state_manager.hpp" +#include "utils/translation.hpp" + +#include + + +using namespace GUIEngine; +using namespace irr::core; + +// ----------------------------------------------------------------------------- +EnterGPNameDialog::EnterGPNameDialog(INewGPListener* listener, + const float w, const float h) + : ModalDialog(w, h), m_listener(listener), m_self_destroy(false) +{ + assert(listener != NULL); + loadFromFile("enter_gp_name_dialog.stkgui"); + + TextBoxWidget* textCtrl = getWidget("textfield"); + assert(textCtrl != NULL); + textCtrl->setFocusForPlayer(PLAYER_ID_GAME_MASTER); +} + +// ----------------------------------------------------------------------------- +EnterGPNameDialog::~EnterGPNameDialog() +{ + // FIXME: what is this code for? + TextBoxWidget* textCtrl = getWidget("textfield"); + textCtrl->getIrrlichtElement()->remove(); + textCtrl->clearListeners(); +} + +// ----------------------------------------------------------------------------- +GUIEngine::EventPropagation EnterGPNameDialog::processEvent(const std::string& eventSource) +{ + if (eventSource == "cancel") + { + dismiss(); + return GUIEngine::EVENT_BLOCK; + } + return GUIEngine::EVENT_LET; +} + +// ----------------------------------------------------------------------------- +void EnterGPNameDialog::onEnterPressedInternal() +{ + //Cancel button pressed + ButtonWidget* cancelButton = getWidget("cancel"); + if (GUIEngine::isFocusedForPlayer(cancelButton, PLAYER_ID_GAME_MASTER)) + { + std::string fakeEvent = "cancel"; + processEvent(fakeEvent); + return; + } + + //Otherwise, see if we can accept the new name + TextBoxWidget* textCtrl = getWidget("textfield"); + assert(textCtrl != NULL); + stringw name = textCtrl->getText().trim(); + if (name.size() > 0) + { + // check for duplicate names + for (int i = 0; i < grand_prix_manager->getNumberOfGrandPrix(); i++) + { + const GrandPrixData* gp = grand_prix_manager->getGrandPrix(i); + if (gp->getName() == name) + { + LabelWidget* label = getWidget("title"); + assert(label != NULL); + label->setText(_("Another grand prix with this name already exists."), false); + sfx_manager->quickSound("anvil"); + return; + } + } + + // It's unsafe to delete from inside the event handler so we do it + // in onUpdate (which checks for m_self_destroy) + m_self_destroy = true; + } + else + { + LabelWidget* label = getWidget("title"); + assert(label != NULL); + label->setText(_("Cannot add a grand prix with this name"), false); + sfx_manager->quickSound("anvil"); + } +} + +// ----------------------------------------------------------------------------- +void EnterGPNameDialog::onUpdate(float dt) +{ + // It's unsafe to delete from inside the event handler so we do it here + if (m_self_destroy) + { + TextBoxWidget* textCtrl = getWidget("textfield"); + stringw name = textCtrl->getText().trim(); + + // irrLicht is too stupid to remove focus from deleted widgets + // so do it by hand + GUIEngine::getGUIEnv()->removeFocus( textCtrl->getIrrlichtElement() ); + GUIEngine::getGUIEnv()->removeFocus( m_irrlicht_window ); + + // we will destroy the dialog before notifying the listener to be safer. + // but in order not to crash we must make a local copy of the listern + // otherwise we will crash + INewGPListener* listener = m_listener; + + ModalDialog::dismiss(); + + if (listener != NULL) + listener->onNewGPWithName(name); + } +} diff --git a/src/states_screens/dialogs/enter_gp_name_dialog.hpp b/src/states_screens/dialogs/enter_gp_name_dialog.hpp new file mode 100644 index 000000000..a9a5cc42d --- /dev/null +++ b/src/states_screens/dialogs/enter_gp_name_dialog.hpp @@ -0,0 +1,70 @@ +// SuperTuxKart - a fun racing game with go-kart +// Copyright (C) 2014 Marc Coll +// +// This program 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. +// +// This program 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 this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + + +#ifndef HEADER_ENTER_GP_NAME_DIALOG_HPP +#define HEADER_ENTER_GP_NAME_DIALOG_HPP + +#include "guiengine/modaldialog.hpp" + +#include + + +namespace GUIEngine +{ + class TextBoxWidget; + class ButtonWidget; + class LabelWidget; +} + +/** + * \brief Dialog that allows the player to enter the name for a new grand prix + * \ingroup states_screens + */ +class EnterGPNameDialog : public GUIEngine::ModalDialog +{ + +public: + + class INewGPListener + { + public: + virtual void onNewGPWithName(const irr::core::stringw& newName) = 0; + virtual ~INewGPListener(){} + }; + +private: + + INewGPListener* m_listener; + bool m_self_destroy; + +public: + + /** + * Creates a modal dialog with given percentage of screen width and height + */ + EnterGPNameDialog(INewGPListener* listener, const float percentWidth, + const float percentHeight); + ~EnterGPNameDialog(); + + void onEnterPressedInternal(); + GUIEngine::EventPropagation processEvent(const std::string& eventSource); + + virtual void onUpdate(float dt); +}; + +#endif diff --git a/src/states_screens/edit_gp_screen.cpp b/src/states_screens/edit_gp_screen.cpp new file mode 100644 index 000000000..d692eb7e4 --- /dev/null +++ b/src/states_screens/edit_gp_screen.cpp @@ -0,0 +1,334 @@ +// SuperTuxKart - a fun racing game with go-kart +// Copyright (C) 2014 Marc Coll +// +// This program 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. +// +// This program 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 this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +#include "states_screens/edit_gp_screen.hpp" + +#include "graphics/irr_driver.hpp" +#include "guiengine/CGUISpriteBank.h" +#include "guiengine/widgets/dynamic_ribbon_widget.hpp" +#include "guiengine/widgets/icon_button_widget.hpp" +#include "guiengine/widgets/label_widget.hpp" +#include "guiengine/widgets/list_widget.hpp" +#include "race/grand_prix_data.hpp" +#include "states_screens/edit_track_screen.hpp" +#include "states_screens/state_manager.hpp" +#include "tracks/track.hpp" +#include "tracks/track_manager.hpp" + + +using namespace GUIEngine; + +DEFINE_SCREEN_SINGLETON( EditGPScreen ); + +// ----------------------------------------------------------------------------- +EditGPScreen::EditGPScreen() + : Screen("gpedit.stkgui"), m_gp(NULL), m_list(NULL), m_icon_bank(NULL), + m_selected(-1), m_modified(false) +{ + +} + +// ----------------------------------------------------------------------------- +EditGPScreen::~EditGPScreen() +{ + delete m_icon_bank; +} + +// ----------------------------------------------------------------------------- +void EditGPScreen::setSelectedGP(GrandPrixData* gp) +{ + assert(gp != NULL); + m_gp = gp; +} + +// ----------------------------------------------------------------------------- +void EditGPScreen::loadedFromFile() +{ + if (m_icon_bank == NULL) + m_icon_bank = new irr::gui::STKModifiedSpriteBank(GUIEngine::getGUIEnv()); + + m_list = getWidget("tracks"); + assert(m_list != NULL); + m_list->addColumn(_("Track"), 3); + m_list->addColumn(_("Laps"), 1); + m_list->addColumn(_("Reversed"), 1); +} + +// ----------------------------------------------------------------------------- +void EditGPScreen::eventCallback(GUIEngine::Widget* widget, const std::string& name, + const int playerID) +{ + setSelected(m_list->getSelectionID()); + + if (name == "tracks") + { + m_action = "edit"; + edit(); + } + else if (name == "menu") + { + RibbonWidget* menu = getWidget("menu"); + assert(menu != NULL); + m_action = menu->getSelectionIDString(PLAYER_ID_GAME_MASTER); + + if (m_action == "up") + { + if (canMoveUp()) + { + m_gp->moveUp(m_selected--); + loadList(m_selected); + setModified(true); + } + } + else if (m_action == "down") + { + if (canMoveDown()) + { + m_gp->moveDown(m_selected++); + loadList(m_selected); + setModified(true); + } + } + else if (m_action == "add" || m_action == "edit") + { + if (m_action == "edit") + { + edit(); + } + else + { + EditTrackScreen* edit = EditTrackScreen::getInstance(); + assert(edit != NULL); + //By default, 3 laps and no reversing + edit->setSelection(NULL, 3, false); + StateManager::get()->pushScreen(edit); + } + } + else if (m_action == "remove") + { + if (m_selected >= 0 && m_selected < m_list->getItemCount()) + { + new MessageDialog( + _("Are you sure you want to remove '%s'?", + m_gp->getTrackName(m_selected).c_str()), + MessageDialog::MESSAGE_DIALOG_CONFIRM, + this, false); + } + } + else if (m_action == "save") + { + save(); + } + } + else if (name == "back") + { + if (m_modified) + { + m_action = "back"; + new MessageDialog( + _("Do you want to save your changes?"), + MessageDialog::MESSAGE_DIALOG_CONFIRM, + this, false); + } + else + { + back(); + } + } +} + +// ----------------------------------------------------------------------------- +void EditGPScreen::init() +{ + if (m_action.empty()) + { + LabelWidget* header = getWidget("title"); + assert(header != NULL); + header->setText(m_gp->getName(), true); + + IconButtonWidget* button = getWidget("save"); + assert(button != NULL); + button->setDeactivated(); + + loadList(0); + setModified(false); + } + else + { + EditTrackScreen* edit = EditTrackScreen::getInstance(); + assert(edit != NULL); + + if (edit->getResult()) + { + if (m_action == "add") + { + m_gp->addTrack(edit->getTrack(), edit->getLaps(), edit->getReverse(), + m_selected); + setSelected(m_selected + 1); + } + else if (m_action == "edit") + { + m_gp->editTrack(m_selected, edit->getTrack(), edit->getLaps(), + edit->getReverse()); + } + setModified(true); + } + loadList(m_selected); + m_action.clear(); + } +} + +// ----------------------------------------------------------------------------- +void EditGPScreen::onConfirm() +{ + ModalDialog::dismiss(); + if (m_action == "remove") + { + m_gp->remove(m_selected); + setSelected(m_selected >= m_gp->getNumberOfTracks() ? + m_gp->getNumberOfTracks() - 1 : m_selected); + loadList(m_selected); + setModified(true); + } + else if (m_action == "back") + { + save(); + back(); + } +} + +// ----------------------------------------------------------------------------- +void EditGPScreen::onCancel() +{ + ModalDialog::dismiss(); + if (m_action == "back") + back(); +} + +// ----------------------------------------------------------------------------- +void EditGPScreen::loadList(const int selected) +{ + m_list->clear(); + m_icons.clear(); + m_icon_bank->clear(); + m_icon_bank->scaleToHeight (64); + m_list->setIcons(m_icon_bank, 64); + + for (unsigned int i = 0; i < m_gp->getNumberOfTracks(); i++) + { + std::vector row; + + Track* t = track_manager->getTrack(m_gp->getTrackId(i)); + assert(t != NULL); + video::ITexture* screenShot = irr_driver->getTexture(t->getScreenshotFile()); + assert(screenShot != NULL); + m_icons.push_back(m_icon_bank->addTextureAsSprite(screenShot)); + + row.push_back(GUIEngine::ListWidget::ListCell( + _LTR(m_gp->getTrackName(i).c_str()), m_icons[i], 3, false)); + row.push_back(GUIEngine::ListWidget::ListCell( + StringUtils::toWString(m_gp->getLaps(i)), -1, 1, true)); + row.push_back(GUIEngine::ListWidget::ListCell( + m_gp->getReverse(i) ? _("Yes") : _("No"), -1, 1, true)); + + m_list->addItem(m_gp->getId(), row); + } + m_list->setIcons(m_icon_bank); + + if (selected < m_list->getItemCount()) + { + m_list->setSelectionID(selected); + setSelected(selected); + } +} + +// ----------------------------------------------------------------------------- +void EditGPScreen::setModified(const bool modified) +{ + m_modified = modified; + + IconButtonWidget* save_button = getWidget("save"); + assert(save_button != NULL); + if (modified) + save_button->setActivated(); + else + save_button->setDeactivated(); +} + +// ----------------------------------------------------------------------------- +void EditGPScreen::setSelected(const int selected) +{ + IconButtonWidget* button_up = getWidget("up"); + assert(button_up != NULL); + IconButtonWidget* button_down = getWidget("down"); + assert(button_down != NULL); + + m_selected = selected; +} + +// ----------------------------------------------------------------------------- +void EditGPScreen::edit() +{ + EditTrackScreen* edit_screen = EditTrackScreen::getInstance(); + assert(edit_screen != NULL); + + if (m_selected >= 0 && m_selected < m_list->getItemCount()) + { + edit_screen->setSelection(track_manager->getTrack( + m_gp->getTrackId(m_selected)), + m_gp->getLaps(m_selected), + m_gp->getReverse(m_selected)); + StateManager::get()->pushScreen(edit_screen); + } +} + +// ----------------------------------------------------------------------------- +bool EditGPScreen::save() +{ + if (m_gp->writeToFile()) + { + setModified(false); + return true; + } + else + { + new MessageDialog( + _("An error occurred while trying to save your grand prix"), + MessageDialog::MESSAGE_DIALOG_OK, NULL, false); + return false; + } +} + +// ----------------------------------------------------------------------------- +void EditGPScreen::back () +{ + m_action.clear(); + m_modified = false; + StateManager::get()->popMenu(); +} + +// ----------------------------------------------------------------------------- +bool EditGPScreen::canMoveUp() const +{ + return (m_selected > 0 && m_selected < m_list->getItemCount()); +} + +// ----------------------------------------------------------------------------- +bool EditGPScreen::canMoveDown() const +{ + return (m_selected >= 0 && m_selected < (m_list->getItemCount() - 1)); +} diff --git a/src/states_screens/edit_gp_screen.hpp b/src/states_screens/edit_gp_screen.hpp new file mode 100644 index 000000000..62b260b1e --- /dev/null +++ b/src/states_screens/edit_gp_screen.hpp @@ -0,0 +1,85 @@ +// SuperTuxKart - a fun racing game with go-kart +// Copyright (C) 2014 Marc Coll +// +// This program 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. +// +// This program 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 this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +#ifndef HEADER_EDIT_GP_SCREEN_HPP +#define HEADER_EDIT_GP_SCREEN_HPP + +#include "guiengine/screen.hpp" +#include "guiengine/widgets/list_widget.hpp" +#include "states_screens/dialogs/message_dialog.hpp" + +#include + + +namespace GUIEngine { class Widget; } +namespace irr { namespace gui { class STKModifiedSpriteBank; } } + +class GrandPrixData; + +/** + * \brief screen where the user can edit a grand prix + * \ingroup states_screens + */ +class EditGPScreen : + public GUIEngine::Screen, + public GUIEngine::ScreenSingleton, + public MessageDialog::IConfirmDialogListener +{ + friend class GUIEngine::ScreenSingleton; + + EditGPScreen(); + + void onConfirm(); + void onCancel(); + + void loadList(const int selected); + void setModified(const bool modified); + void setSelected(const int selected); + void edit(); + bool save(); + void back(); + + bool canMoveUp() const; + bool canMoveDown() const; + + GrandPrixData* m_gp; + GUIEngine::ListWidget* m_list; + irr::gui::STKModifiedSpriteBank* m_icon_bank; + std::vector m_icons; + int m_selected; + bool m_modified; + + std::string m_action; + +public: + + ~EditGPScreen(); + + void setSelectedGP(GrandPrixData* gp); + + /** \brief implement callback from parent class GUIEngine::Screen */ + virtual void loadedFromFile() OVERRIDE; + + /** \brief implement callback from parent class GUIEngine::Screen */ + virtual void eventCallback(GUIEngine::Widget* widget, const std::string& name, + const int playerID) OVERRIDE; + + /** \brief implement callback from parent class GUIEngine::Screen */ + virtual void init() OVERRIDE; +}; + +#endif diff --git a/src/states_screens/edit_track_screen.cpp b/src/states_screens/edit_track_screen.cpp new file mode 100644 index 000000000..7259a5623 --- /dev/null +++ b/src/states_screens/edit_track_screen.cpp @@ -0,0 +1,240 @@ +// SuperTuxKart - a fun racing game with go-kart +// Copyright (C) 2014 Marc Coll +// +// This program 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. +// +// This program 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 this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +#include "states_screens/edit_track_screen.hpp" + +#include "guiengine/widgets/button_widget.hpp" +#include "guiengine/widgets/check_box_widget.hpp" +#include "guiengine/widgets/dynamic_ribbon_widget.hpp" +#include "guiengine/widgets/label_widget.hpp" +#include "guiengine/widgets/ribbon_widget.hpp" +#include "guiengine/widgets/spinner_widget.hpp" +#include "states_screens/state_manager.hpp" +#include "tracks/track.hpp" +#include "tracks/track_manager.hpp" + + +using namespace GUIEngine; +using namespace irr::core; + +const char* EditTrackScreen::ALL_TRACKS_GROUP_ID = "all"; + +DEFINE_SCREEN_SINGLETON( EditTrackScreen ); + +// ----------------------------------------------------------------------------- +EditTrackScreen::EditTrackScreen() + : Screen("edit_track.stkgui"), m_track_group(ALL_TRACKS_GROUP_ID), + m_track(NULL), m_laps(0), m_reverse(false), m_result(false) +{ + +} + +// ----------------------------------------------------------------------------- +EditTrackScreen::~EditTrackScreen() +{ + +} + +// ----------------------------------------------------------------------------- +void EditTrackScreen::setSelection(Track* track, unsigned int laps, bool reverse) +{ + assert(laps > 0); + m_track = track; + m_laps = laps; + m_reverse = reverse; +} + +// ----------------------------------------------------------------------------- +Track* EditTrackScreen::getTrack() const +{ + return m_track; +} + +// ----------------------------------------------------------------------------- +unsigned int EditTrackScreen::getLaps() const +{ + return m_laps; +} + +// ----------------------------------------------------------------------------- +bool EditTrackScreen::getReverse() const +{ + return m_reverse; +} + +// ----------------------------------------------------------------------------- +bool EditTrackScreen::getResult() const +{ + return m_result; +} + +// ----------------------------------------------------------------------------- +void EditTrackScreen::loadedFromFile() +{ + +} + +// ----------------------------------------------------------------------------- +void EditTrackScreen::eventCallback(GUIEngine::Widget* widget, const std::string& name, + const int playerID) +{ + if (name == "ok") + { + m_result = true; + StateManager::get()->popMenu(); + } + else if (name == "cancel") + { + m_result = false; + StateManager::get()->popMenu(); + } + else if (name == "tracks") + { + DynamicRibbonWidget* tracks = getWidget("tracks"); + assert(tracks != NULL); + selectTrack(tracks->getSelectionIDString(PLAYER_ID_GAME_MASTER)); + } + else if (name == "trackgroups") + { + RibbonWidget* tabs = getWidget("trackgroups"); + assert(tabs != NULL); + m_track_group = tabs->getSelectionIDString(PLAYER_ID_GAME_MASTER); + loadTrackList(); + } + else if (name == "laps") + { + SpinnerWidget* laps = getWidget("laps"); + assert(laps != NULL); + m_laps = laps->getValue(); + } + else if (name == "reverse") + { + CheckBoxWidget* reverse = getWidget("reverse"); + assert(reverse != NULL); + m_reverse = reverse->getState(); + } +} + +// ----------------------------------------------------------------------------- +void EditTrackScreen::beforeAddingWidget() +{ + RibbonWidget* tabs = getWidget("trackgroups"); + assert (tabs != NULL); + + tabs->clearAllChildren(); + + const std::vector& groups = track_manager->getAllTrackGroups(); + if (groups.size() > 1) + { + tabs->addTextChild(_("All"), ALL_TRACKS_GROUP_ID); + for (unsigned int i = 0; i < groups.size(); i++) + tabs->addTextChild(_(groups[i].c_str()), groups[i]); + } +} + +// ----------------------------------------------------------------------------- +void EditTrackScreen::init() +{ + RibbonWidget* tabs = getWidget("trackgroups"); + assert (tabs != NULL); + SpinnerWidget* laps = getWidget("laps"); + assert(laps != NULL); + CheckBoxWidget* reverse = getWidget("reverse"); + assert(reverse != NULL); + + if (m_track_group.empty()) + tabs->select (ALL_TRACKS_GROUP_ID, PLAYER_ID_GAME_MASTER); + else + tabs->select (m_track_group, PLAYER_ID_GAME_MASTER); + laps->setValue(m_laps); + reverse->setState(m_reverse); + + loadTrackList(); + if (m_track == NULL) + selectTrack(""); + else + selectTrack(m_track->getIdent()); +} + +// ----------------------------------------------------------------------------- +void EditTrackScreen::loadTrackList() +{ + bool belongsToGroup; + + DynamicRibbonWidget* tracks_widget = getWidget("tracks"); + assert(tracks_widget != NULL); + + tracks_widget->clearItems(); + + for (unsigned int i = 0; i < track_manager->getNumberOfTracks(); i++) + { + Track* t = track_manager->getTrack(i); + const std::vector& groups = t->getGroups(); + belongsToGroup = (m_track_group.empty() || m_track_group == ALL_TRACKS_GROUP_ID || + std::find(groups.begin(), groups.end(), m_track_group) != groups.end()); + if (!t->isArena() && !t->isSoccer() && !t->isInternal() && belongsToGroup) + { + tracks_widget->addItem(translations->fribidize(t->getName()), t->getIdent(), + t->getScreenshotFile(), 0, IconButtonWidget::ICON_PATH_TYPE_ABSOLUTE ); + } + } + + tracks_widget->updateItemDisplay(); +} + +// ----------------------------------------------------------------------------- +void EditTrackScreen::selectTrack(const std::string& id) +{ + DynamicRibbonWidget* tracks = getWidget("tracks"); + assert(tracks != NULL); + LabelWidget* selected_track = getWidget("selected_track"); + assert(selected_track != NULL); + SpinnerWidget* laps = getWidget("laps"); + assert(laps != NULL); + LabelWidget* label_reverse = getWidget("reverse_label"); + assert(label_reverse != NULL); + CheckBoxWidget* reverse = getWidget("reverse"); + assert(reverse != NULL); + ButtonWidget* ok_button = getWidget("ok"); + assert(ok_button != NULL); + + m_track = track_manager->getTrack(id); + if (m_track != NULL) + { + tracks->setSelection(m_track->getIdent(), PLAYER_ID_GAME_MASTER, true); + selected_track->setText(m_track->getName(), true); + + laps->setValue(m_laps); + + reverse->setVisible(m_track->reverseAvailable()); + label_reverse->setVisible(m_track->reverseAvailable()); + + ok_button->setActivated(); + } + else + { + tracks->setSelection("", PLAYER_ID_GAME_MASTER, true); + selected_track->setText(_("Select a track"), true); + + laps->setValue(3); + + reverse->setVisible(true); + reverse->setState(false); + + ok_button->setDeactivated(); + } +} diff --git a/src/states_screens/edit_track_screen.hpp b/src/states_screens/edit_track_screen.hpp new file mode 100644 index 000000000..a5edb982b --- /dev/null +++ b/src/states_screens/edit_track_screen.hpp @@ -0,0 +1,78 @@ +// SuperTuxKart - a fun racing game with go-kart +// Copyright (C) 2014 Marc Coll +// +// This program 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. +// +// This program 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 this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +#ifndef HEADER_EDIT_TRACK_SCREEN_HPP +#define HEADER_EDIT_TRACK_SCREEN_HPP + +#include "guiengine/screen.hpp" + + +namespace GUIEngine { class Widget; } + +namespace irr { namespace gui { class STKModifiedSpriteBank; } } + +class Track; + +/** + * \brief screen where the user can edit the details of a track inside a grand prix + * \ingroup states_screens + */ +class EditTrackScreen : + public GUIEngine::Screen, + public GUIEngine::ScreenSingleton +{ + friend class GUIEngine::ScreenSingleton; + + static const char* ALL_TRACKS_GROUP_ID; + + EditTrackScreen(); + + void loadTrackList(); + void selectTrack(const std::string& id); + + std::string m_track_group; + + Track* m_track; + unsigned int m_laps; + bool m_reverse; + bool m_result; + +public: + + ~EditTrackScreen(); + + void setSelection(Track* track, unsigned int laps, bool reverse); + Track* getTrack() const; + unsigned int getLaps() const; + bool getReverse() const; + bool getResult() const; + + /** \brief implement callback from parent class GUIEngine::Screen */ + virtual void beforeAddingWidget() OVERRIDE; + + /** \brief implement callback from parent class GUIEngine::Screen */ + virtual void loadedFromFile() OVERRIDE; + + /** \brief implement callback from parent class GUIEngine::Screen */ + virtual void eventCallback(GUIEngine::Widget* widget, const std::string& name, + const int playerID) OVERRIDE; + + /** \brief implement callback from parent class GUIEngine::Screen */ + virtual void init() OVERRIDE; +}; + +#endif diff --git a/src/states_screens/grand_prix_editor_screen.cpp b/src/states_screens/grand_prix_editor_screen.cpp new file mode 100644 index 000000000..238c5e94b --- /dev/null +++ b/src/states_screens/grand_prix_editor_screen.cpp @@ -0,0 +1,286 @@ +// SuperTuxKart - a fun racing game with go-kart +// Copyright (C) 2014 Marc Coll +// +// This program 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. +// +// This program 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 this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +#include "states_screens/grand_prix_editor_screen.hpp" + +#include "graphics/irr_driver.hpp" +#include "guiengine/widget.hpp" +#include "guiengine/widgets/label_widget.hpp" +#include "guiengine/widgets/dynamic_ribbon_widget.hpp" +#include "guiengine/widgets/icon_button_widget.hpp" +#include "io/file_manager.hpp" +#include "race/grand_prix_data.hpp" +#include "race/grand_prix_manager.hpp" +#include "states_screens/state_manager.hpp" +#include "states_screens/edit_gp_screen.hpp" +#include "states_screens/dialogs/enter_gp_name_dialog.hpp" +#include "states_screens/dialogs/gp_info_dialog.hpp" +#include "states_screens/dialogs/track_info_dialog.hpp" +#include "tracks/track.hpp" +#include "tracks/track_manager.hpp" +#include "utils/translation.hpp" + + +using namespace GUIEngine; +using namespace irr::core; + +DEFINE_SCREEN_SINGLETON( GrandPrixEditorScreen ); + +// ----------------------------------------------------------------------------- +GrandPrixEditorScreen::GrandPrixEditorScreen() + : Screen("gpeditor.stkgui"), m_selection(NULL) +{ +} + +// ----------------------------------------------------------------------------- +void GrandPrixEditorScreen::loadedFromFile() +{ + +} + +// ----------------------------------------------------------------------------- +void GrandPrixEditorScreen::eventCallback(Widget* widget, const std::string& name, const int playerID) +{ + DynamicRibbonWidget* gplist_widget = getWidget("gplist"); + assert (gplist_widget != NULL); + std::string selected = gplist_widget->getSelectionIDString(PLAYER_ID_GAME_MASTER); + if (!selected.empty()) + setSelection (grand_prix_manager->getGrandPrix(selected)); + + if (name == "menu") + { + RibbonWidget* menu = getWidget("menu"); + assert(menu != NULL); + m_action = menu->getSelectionIDString(PLAYER_ID_GAME_MASTER); + + if (m_action == "new" || m_action == "copy") + { + new EnterGPNameDialog(this, 0.5f, 0.4f); + } + else if (m_action == "edit") + { + if (m_selection->isEditable()) + { + showEditScreen(m_selection); + } + else + { + new MessageDialog( + _("You can't edit the '%s' grand prix.\nYou might want to copy it first", + m_selection->getName().c_str()), + MessageDialog::MESSAGE_DIALOG_OK, NULL, false); + } + } + else if (m_action == "remove") + { + if (m_selection->isEditable()) + { + new MessageDialog( + _("Are you sure you want to remove '%s'?", m_selection->getName().c_str()), + MessageDialog::MESSAGE_DIALOG_CONFIRM, + this, false); + } + else + { + new MessageDialog( + _("You can't remove '%s'.", m_selection->getName().c_str()), + MessageDialog::MESSAGE_DIALOG_OK, NULL, false); + } + } + else if (m_action == "rename") + { + if (m_selection->isEditable()) + { + new EnterGPNameDialog(this, 0.5f, 0.4f); + } + else + { + new MessageDialog( + _("You can't rename '%s'.", m_selection->getName().c_str()), + MessageDialog::MESSAGE_DIALOG_OK, NULL, false); + } + } + } + else if (name == "back") + { + StateManager::get()->escapePressed(); + } +} + +// ----------------------------------------------------------------------------- +void GrandPrixEditorScreen::init() +{ + if (grand_prix_manager->getNumberOfGrandPrix() > 0) + { + if (m_selection == NULL) + { + loadGPList(); + setSelection (grand_prix_manager->getGrandPrix(0)); + } + else + { + std::string id = m_selection->getId(); + grand_prix_manager->reload(); + loadGPList(); + m_selection = grand_prix_manager->editGrandPrix(id); + m_selection->reload(); + setSelection (m_selection); + } + } + else + { + loadGPList(); + } +} + +// ----------------------------------------------------------------------------- +void GrandPrixEditorScreen::setSelection (const GrandPrixData* gpdata) +{ + LabelWidget* gpname_widget = getWidget("gpname"); + assert(gpname_widget != NULL); + DynamicRibbonWidget* gplist_widget = getWidget("gplist"); + assert (gplist_widget != NULL); + + m_selection = grand_prix_manager->editGrandPrix(gpdata->getId()); + gpname_widget->setText (gpdata->getName(), true); + gplist_widget->setSelection(m_selection->getId(), PLAYER_ID_GAME_MASTER, true); + loadTrackList (gpdata->getId()); +} + +// ----------------------------------------------------------------------------- +void GrandPrixEditorScreen::loadTrackList (const std::string& gpname) +{ + if (gpname.empty()) + return; + + DynamicRibbonWidget* tracks_widget = getWidget("tracks"); + assert(tracks_widget != NULL); + + const GrandPrixData* gp = grand_prix_manager->getGrandPrix(gpname); + const std::vector& tracks = gp->getTrackNames(); + + tracks_widget->clearItems(); + tracks_widget->setItemCountHint(tracks.size()); + for (unsigned int t = 0; t < tracks.size(); t++) + { + Track* curr = track_manager->getTrack(tracks[t]); + if (curr == NULL) + { + Log::warn("GrandPrixEditor", + "Grand Prix '%s' refers to track '%s', which does not exist\n", + gp->getId().c_str(), tracks[t].c_str()); + } + else + { + tracks_widget->addItem( + StringUtils::toWString(t + 1) + ". " + translations->fribidize(curr->getName()), + curr->getIdent(), curr->getScreenshotFile(), 0, + IconButtonWidget::ICON_PATH_TYPE_ABSOLUTE ); + } + } + + tracks_widget->updateItemDisplay(); +} + +// ----------------------------------------------------------------------------- +void GrandPrixEditorScreen::loadGPList() +{ + DynamicRibbonWidget* gplist_widget = getWidget("gplist"); + assert(gplist_widget != NULL); + + // Reset GP list everytime (accounts for locking changes, etc.) + gplist_widget->clearItems(); + + // Build GP list + for (unsigned int i = 0; i < grand_prix_manager->getNumberOfGrandPrix(); i++) + { + const GrandPrixData* gp = grand_prix_manager->getGrandPrix(i); + const std::vector& tracks = gp->getTrackNames(); + + std::vector sshot_files; + for (unsigned int t=0; tgetTrack(tracks[t]); + if (track == NULL) + { + Log::warn("GrandPrixEditor", + "Grand Prix '%s' refers to track '%s', which does not exist\n", + gp->getId().c_str(), tracks[t].c_str()); + } + else + { + sshot_files.push_back(track->getScreenshotFile()); + } + } + if (sshot_files.size() == 0) + { + Log::warn("GrandPrixEditor", + "Grand Prix '%s' does not contain any valid track\n", + gp->getId().c_str()); + sshot_files.push_back("gui/main_help.png"); + } + + gplist_widget->addAnimatedItem(translations->fribidize(gp->getName()), gp->getId(), + sshot_files, 2.0f, 0, IconButtonWidget::ICON_PATH_TYPE_ABSOLUTE ); + } + + gplist_widget->updateItemDisplay(); +} + +// ----------------------------------------------------------------------------- +void GrandPrixEditorScreen::showEditScreen(GrandPrixData* gp) +{ + assert(gp != NULL); + EditGPScreen* edit = EditGPScreen::getInstance(); + edit->setSelectedGP(gp); + StateManager::get()->pushScreen(edit); +} + +// ----------------------------------------------------------------------------- +void GrandPrixEditorScreen::onNewGPWithName(const stringw& newName) +{ + if (m_action == "copy") + { + setSelection(grand_prix_manager->copy(m_selection->getId(), newName)); + } + else if (m_action == "rename") + { + m_selection->setName(newName); + m_selection->writeToFile(); + } + else if (m_action == "new") + { + setSelection(grand_prix_manager->createNew(newName)); + } + + loadGPList(); + if (m_action != "rename") + showEditScreen(m_selection); +} + +// ----------------------------------------------------------------------------- +void GrandPrixEditorScreen::onConfirm() +{ + if (m_action == "remove") + { + grand_prix_manager->remove(m_selection->getId()); + loadGPList(); + if (grand_prix_manager->getNumberOfGrandPrix() > 0) + setSelection (grand_prix_manager->getGrandPrix(0)); + } + ModalDialog::dismiss(); +} diff --git a/src/states_screens/grand_prix_editor_screen.hpp b/src/states_screens/grand_prix_editor_screen.hpp new file mode 100644 index 000000000..4d763846b --- /dev/null +++ b/src/states_screens/grand_prix_editor_screen.hpp @@ -0,0 +1,68 @@ +// SuperTuxKart - a fun racing game with go-kart +// Copyright (C) 2014 Marc Coll +// +// This program 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. +// +// This program 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 this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +#ifndef HEADER_GRAND_PRIX_EDITOR_SCREEN_HPP +#define HEADER_GRAND_PRIX_EDITOR_SCREEN_HPP + +#include "dialogs/enter_gp_name_dialog.hpp" +#include "guiengine/screen.hpp" +#include "states_screens/dialogs/message_dialog.hpp" + + +namespace GUIEngine { class Widget; } + +class GrandPrixData; + +/** + * \brief screen where the user can edit his own grand prix + * \ingroup states_screens + */ +class GrandPrixEditorScreen : + public GUIEngine::Screen, + public GUIEngine::ScreenSingleton, + public EnterGPNameDialog::INewGPListener, + public MessageDialog::IConfirmDialogListener +{ + friend class GUIEngine::ScreenSingleton; + + GrandPrixEditorScreen(); + + void setSelection(const GrandPrixData* gpdata); + void loadGPList(); + void loadTrackList(const std::string& gpname); + void showEditScreen(GrandPrixData* gp); + + void onNewGPWithName(const irr::core::stringw& newName); + void onConfirm(); + + GrandPrixData* m_selection; + std::string m_action; + +public: + + /** \brief implement callback from parent class GUIEngine::Screen */ + virtual void loadedFromFile() OVERRIDE; + + /** \brief implement callback from parent class GUIEngine::Screen */ + virtual void eventCallback(GUIEngine::Widget* widget, const std::string& name, + const int playerID) OVERRIDE; + + /** \brief implement callback from parent class GUIEngine::Screen */ + virtual void init() OVERRIDE; +}; + +#endif diff --git a/src/states_screens/main_menu_screen.cpp b/src/states_screens/main_menu_screen.cpp index bbe0a215c..0bfaeab7b 100644 --- a/src/states_screens/main_menu_screen.cpp +++ b/src/states_screens/main_menu_screen.cpp @@ -39,6 +39,7 @@ #include "online/request_manager.hpp" #include "states_screens/addons_screen.hpp" #include "states_screens/credits.hpp" +#include "states_screens/grand_prix_editor_screen.hpp" #include "states_screens/help_screen_1.hpp" #include "states_screens/login_screen.hpp" #include "states_screens/offline_kart_selection.hpp" @@ -158,7 +159,7 @@ void MainMenuScreen::onUpdate(float delta) } else // now must be either logging in or logging out m_online->setDeactivated(); - + m_online->setLabel(CurrentUser::get()->getID() ? _("Online") : _("Login" ) ); IconButtonWidget* addons_icon = getWidget("addons"); @@ -413,7 +414,7 @@ void MainMenuScreen::eventCallback(Widget* widget, const std::string& name, { // Don't go to addons if there is no internet, unless some addons are // already installed (so that you can delete addons without being online). - if(UserConfigParams::m_internet_status!=RequestManager::IPERM_ALLOWED && + if(UserConfigParams::m_internet_status!=RequestManager::IPERM_ALLOWED && !addons_manager->anyAddonsInstalled()) { new MessageDialog(_("You can not download addons without internet access. " @@ -424,6 +425,10 @@ void MainMenuScreen::eventCallback(Widget* widget, const std::string& name, } StateManager::get()->pushScreen(AddonsScreen::getInstance()); } + else if (selection == "gpEditor") + { + StateManager::get()->pushScreen(GrandPrixEditorScreen::getInstance()); + } } // eventCallback // ---------------------------------------------------------------------------- From 89e070c7f31d209a87a106ca6866e50772c98e21 Mon Sep 17 00:00:00 2001 From: Marianne Gagnon Date: Thu, 20 Mar 2014 21:30:10 -0400 Subject: [PATCH 36/38] Revert dubious change from GP patch --- src/race/grand_prix_data.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/race/grand_prix_data.cpp b/src/race/grand_prix_data.cpp index 23cfe6b1a..73d9d9cfe 100644 --- a/src/race/grand_prix_data.cpp +++ b/src/race/grand_prix_data.cpp @@ -166,7 +166,7 @@ bool GrandPrixData::writeToFile() if (file.is_open()) { file << L"\n\n\n"; - for (unsigned int i = 0; i < getNumberOfTracks(); i++) + for (unsigned int i = 0; i < m_tracks.size(); i++) { file << L"\tgetTrack(m_tracks[i]); @@ -230,7 +230,7 @@ bool GrandPrixData::isTrackAvailable(const std::string &id) const void GrandPrixData::getLaps(std::vector *laps) const { laps->clear(); - for(unsigned int i=0; i< getNumberOfTracks(); i++) + for (unsigned int i = 0; i< m_tracks.size(); i++) if(isTrackAvailable(m_tracks[i])) laps->push_back(m_laps[i]); } // getLaps @@ -239,7 +239,7 @@ void GrandPrixData::getLaps(std::vector *laps) const void GrandPrixData::getReverse(std::vector *reverse) const { reverse->clear(); - for(unsigned int i=0; i< getNumberOfTracks(); i++) + for (unsigned int i = 0; i< m_tracks.size(); i++) if(isTrackAvailable(m_tracks[i])) reverse->push_back(m_reversed[i]); } // getReverse @@ -361,7 +361,7 @@ void GrandPrixData::remove(const unsigned int track) const std::vector& GrandPrixData::getTrackNames() const { m_really_available_tracks.clear(); - for(unsigned int i=0; i< getNumberOfTracks(); i++) + for (unsigned int i = 0; i < m_tracks.size(); i++) { if(isTrackAvailable(m_tracks[i])) m_really_available_tracks.push_back(m_tracks[i]); From 9728226ee349df4867c28d1d9f40c5633112d2fc Mon Sep 17 00:00:00 2001 From: Vincent Lejeune Date: Fri, 21 Mar 2014 15:58:21 +0100 Subject: [PATCH 37/38] Forgot to dereference a value. --- src/graphics/stkmesh.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/graphics/stkmesh.cpp b/src/graphics/stkmesh.cpp index b267d41d3..8acc40732 100644 --- a/src/graphics/stkmesh.cpp +++ b/src/graphics/stkmesh.cpp @@ -422,7 +422,7 @@ void drawCaustics(const GLMesh &mesh, const core::matrix4 & ModelViewProjectionM glTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_RGBA, swizzleMask); } if (!CausticTex) - irr_driver->getTexture(file_manager->getAsset("textures/caustics.png").c_str()); + CausticTex = irr_driver->getTexture(file_manager->getAsset("textures/caustics.png").c_str()); setTexture(MeshShader::CausticsShader::TU_caustictex, getTextureGLuint(CausticTex), GL_LINEAR, GL_LINEAR_MIPMAP_LINEAR, true); MeshShader::CausticsShader::setUniforms(ModelViewProjectionMatrix, dir, dir2, core::vector2df(UserConfigParams::m_width, UserConfigParams::m_height)); From 33b38803207ec687487786096f6f476318e206d7 Mon Sep 17 00:00:00 2001 From: Marianne Gagnon Date: Fri, 21 Mar 2014 18:56:47 -0400 Subject: [PATCH 38/38] Add the author of the GP editor to the credits --- data/CREDITS | Bin 12896 -> 12960 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/data/CREDITS b/data/CREDITS index b358814c2b3e38b26c38b64200b32e33ba814731..0ab04b4de6117df74e296193914b98bdddc98e54 100644 GIT binary patch delta 32 ocmaEmvLJQCKi0`A+Cr19IApj}8B!QB8A=%P8HzT4XU)_B0K>-$xBvhE delta 12 TcmZ3G`XFV)Ki185Y)Lu*Dli3t