Explorar el Código

Merge branch 'develop' of https://github.com/vcmi/vcmi into develop

Trying to sort out git tree.
DJWarmonger hace 8 años
padre
commit
5a31cc831d
Se han modificado 44 ficheros con 511 adiciones y 339 borrados
  1. 56 40
      .travis.yml
  2. 3 5
      AI/BattleAI/BattleAI.cpp
  3. 3 3
      AI/VCAI/VCAI.cpp
  4. 1 1
      CCallback.cpp
  5. BIN
      CI/deploy_rsa.enc
  6. 16 0
      CI/get_package_name.sh
  7. 0 0
      CI/linux/before_install.sh
  8. 1 0
      CI/linux/upload_package.sh
  9. 1 1
      CI/mac/before_install.sh
  10. 10 0
      CI/mac/upload_package.sh
  11. 3 0
      CI/mxe/before_install.sh
  12. 10 0
      CI/mxe/upload_package.sh
  13. 33 12
      CMakeLists.txt
  14. 3 0
      ChangeLog
  15. 8 11
      client/CPlayerInterface.cpp
  16. 18 27
      client/battle/CBattleInterface.cpp
  17. 5 2
      client/battle/CBattleInterface.h
  18. 26 19
      config/schemas/spell.json
  19. 2 1
      config/spells/ability.json
  20. 5 3
      config/spells/timed.json
  21. 4 2
      lib/CArtHandler.cpp
  22. 0 5
      lib/CBattleCallback.cpp
  23. 0 1
      lib/CBattleCallback.h
  24. 3 3
      lib/CGameState.cpp
  25. 1 1
      lib/CGameState.h
  26. 0 15
      lib/CStack.cpp
  27. 0 1
      lib/CStack.h
  28. 8 0
      lib/NetPacks.h
  29. 22 7
      lib/NetPacksLib.cpp
  30. 2 2
      lib/VCMI_lib.vcxproj
  31. 21 21
      lib/filesystem/CZipSaver.cpp
  32. 9 9
      lib/filesystem/CZipSaver.h
  33. 1 1
      lib/filesystem/MinizipExtensions.cpp
  34. 1 1
      lib/serializer/CSerializer.h
  35. 2 1
      lib/spells/AdventureSpellMechanics.cpp
  36. 104 91
      lib/spells/CDefaultSpellMechanics.cpp
  37. 3 0
      lib/spells/CDefaultSpellMechanics.h
  38. 36 14
      lib/spells/CSpellHandler.cpp
  39. 31 7
      lib/spells/CSpellHandler.h
  40. 31 13
      server/CGameHandler.cpp
  41. 1 1
      server/CGameHandler.h
  42. 6 2
      test/CMapEditManagerTest.cpp
  43. 6 2
      test/CMapFormatTest.cpp
  44. 15 14
      test/CMemoryBufferTest.cpp

+ 56 - 40
.travis.yml

@@ -1,52 +1,36 @@
 language: cpp
 os:
-  - linux
-  - osx
+- linux
+- osx
 dist: trusty
 sudo: required
-
-before_install:
-  - test $TRAVIS_BRANCH != coverity_scan -o ${TRAVIS_JOB_NUMBER##*.} = 1 || exit 0
-  - if [[ $VCMI_PLATFORM == 'linux' ]]; then . .travis.linux; fi
-  - if [[ $VCMI_PLATFORM == 'mac' ]]; then . .travis.osx; fi
-  - if [[ $VCMI_PLATFORM == 'mxe' ]]; then . .travis.mxe; fi
-
-before_script:
-  - mkdir build
-  - cd build
-  - if [[ $TRAVIS_BRANCH != 'coverity_scan' ]]; then cmake -G "Unix Makefiles" .. $VCMI_CMAKE_FLAGS; fi
-
-script:
-  - test $TRAVIS_BRANCH != coverity_scan || exit 0
-  - if [[ $TRAVIS_OS_NAME == 'osx' ]]; then cd ..; xcodebuild -project osx/osx-vcmibuilder/vcmibuilder.xcodeproj/ -configuration Release CONFIGURATION_BUILD_DIR=..; cd build; fi
-  - make -j2
-
 env:
   matrix:
     - ignore=this
   global:
-    # The next declaration is the encrypted COVERITY_SCAN_TOKEN, created
-    #   via the "travis encrypt" command using the project repo's public key
-    - secure: "NMg+qtQB4DIZ/KqlDeIn3K7A7Ydksdpnbv6Ha9n4bSSA0AT8wlPwbHXvQmiR8qYs6cnz4fyY6NVcBe7X3bdR8jWyPNAS0l0QByqG12q3dBpEtNNn0X5u/GS3wHse5+ObNAF9a83+xACTQj2UdxqHgJ3LFGzdBpQt3kLsc8NDnn8="
+  - secure: NMg+qtQB4DIZ/KqlDeIn3K7A7Ydksdpnbv6Ha9n4bSSA0AT8wlPwbHXvQmiR8qYs6cnz4fyY6NVcBe7X3bdR8jWyPNAS0l0QByqG12q3dBpEtNNn0X5u/GS3wHse5+ObNAF9a83+xACTQj2UdxqHgJ3LFGzdBpQt3kLsc8NDnn8=
 
 matrix:
   exclude:
-    - env: ignore=this
+  - env: ignore=this
   include:
-    - os: linux
-      compiler: clang
-      env: VCMI_PLATFORM='linux' REAL_CC=clang-3.6 REAL_CXX=clang++-3.6 PACKAGE=clang-3.6 SUPPORT=libstdc++-4.8-dev VCMI_CMAKE_FLAGS='-DENABLE_TEST=0'
-    - os: linux
-      compiler: clang
-      env: VCMI_PLATFORM='linux' REAL_CC=clang-3.4 REAL_CXX=clang++-3.4 PACKAGE=clang-3.4 SUPPORT=libstdc++-4.8-dev VCMI_CMAKE_FLAGS='-DENABLE_TEST=0'
-    - os: linux
-      compiler: gcc
-      env: VCMI_PLATFORM='linux' REAL_CC=gcc-4.8   REAL_CXX=g++-4.8     PACKAGE=g++-4.8   SUPPORT= VCMI_CMAKE_FLAGS='-DENABLE_TEST=0'
-    - os: linux
-      env: VCMI_PLATFORM='mxe' MXE_TARGET=i686-w64-mingw32.shared VCMI_CMAKE_FLAGS='-DENABLE_TEST=0'
-      sudo: required
-    - os: osx
-      env: VCMI_PLATFORM='mac'
+  - os: linux
+    compiler: clang
+    env: VCMI_PLATFORM='linux' REAL_CC=clang-3.6 REAL_CXX=clang++-3.6 PACKAGE=clang-3.6
+      SUPPORT=libstdc++-4.8-dev VCMI_CMAKE_FLAGS='-DENABLE_TEST=0'
+  - os: linux
+    compiler: clang
+    env: VCMI_PLATFORM='linux' REAL_CC=clang-3.4 REAL_CXX=clang++-3.4 PACKAGE=clang-3.4
+      SUPPORT=libstdc++-4.8-dev VCMI_CMAKE_FLAGS='-DENABLE_TEST=0'
+  - os: linux
+    compiler: gcc
+    env: VCMI_PLATFORM='linux' REAL_CC=gcc-4.8   REAL_CXX=g++-4.8     PACKAGE=g++-4.8   SUPPORT=
+      VCMI_CMAKE_FLAGS='-DENABLE_TEST=0'
+  - os: linux
+    env: VCMI_PLATFORM='mxe' MXE_TARGET=i686-w64-mingw32.shared VCMI_CMAKE_FLAGS='-DENABLE_TEST=0'
+    sudo: required
+  - os: osx
+    env: VCMI_PLATFORM='mac'
 
 addons:
   coverity_scan:
@@ -54,15 +38,47 @@ addons:
       name: vcmi/vcmi
       description: Build submitted via Travis CI
     notification_email: [email protected]
-    build_command_prepend: "cov-configure --compiler clang-3.6 --comptype clangcc && cov-configure --comptype clangcxx --compiler clang++-3.6 && cmake -G Ninja .. -DCMAKE_BUILD_TYPE=DEBUG -DENABLE_LAUNCHER=0 -DENABLE_TEST=0"
+    build_command_prepend: cov-configure --compiler clang-3.6 --comptype clangcc &&
+      cov-configure --comptype clangcxx --compiler clang++-3.6 && cmake -G Ninja ..
+      -DCMAKE_BUILD_TYPE=DEBUG -DENABLE_LAUNCHER=0 -DENABLE_TEST=0
     build_command: ninja -j 3
     branch_pattern: coverity_scan
 
 notifications:
   email:
     recipients:
-      - [email protected]
+    - [email protected]
     on_success: change
     on_failure: always
   slack:
-    secure: "KHXFe14FFKtw5mErWbj730+utqy7i/3AUobWfAMAGvWI5sJYlhbBU+KvvCoD2SlRQg3mQqgwVw8NBJF1Mffs7WcRmrFFFmuMqZxFLAfKBd3T0CxWpAGfnfNgDmlfV4OfEgQWk1pakEPOymhxbbmLUuCjykZDuTcioxAk0UAHDwY="
+    secure: KHXFe14FFKtw5mErWbj730+utqy7i/3AUobWfAMAGvWI5sJYlhbBU+KvvCoD2SlRQg3mQqgwVw8NBJF1Mffs7WcRmrFFFmuMqZxFLAfKBd3T0CxWpAGfnfNgDmlfV4OfEgQWk1pakEPOymhxbbmLUuCjykZDuTcioxAk0UAHDwY=
+
+before_install:
+- test $TRAVIS_BRANCH != coverity_scan -o ${TRAVIS_JOB_NUMBER##*.} = 1 || exit 0
+- . $TRAVIS_BUILD_DIR/CI/$VCMI_PLATFORM/before_install.sh
+
+before_script:
+- mkdir build
+- cd build
+- if [[ $TRAVIS_BRANCH != 'coverity_scan' ]];
+  then
+    source $TRAVIS_BUILD_DIR/CI/get_package_name.sh;
+    cmake -G "Unix Makefiles" .. $VCMI_CMAKE_FLAGS
+      -DPACKAGE_NAME_SUFFIX:STRING="$VCMI_PACKAGE_NAME_SUFFIX"
+      -DPACKAGE_FILE_NAME:STRING="$VCMI_PACKAGE_FILE_NAME";
+  fi
+  
+script:
+- test $TRAVIS_BRANCH != coverity_scan || exit 0
+- if [[ $TRAVIS_OS_NAME == 'osx' ]];
+  then
+    cd ..;
+    xcodebuild -project osx/osx-vcmibuilder/vcmibuilder.xcodeproj/
+      -configuration Release CONFIGURATION_BUILD_DIR=..;
+    cd build;
+  fi
+- make -j2
+
+after_success:
+- test $TRAVIS_BRANCH != coverity_scan || exit 0
+- . $TRAVIS_BUILD_DIR/CI/$VCMI_PLATFORM/upload_package.sh

+ 3 - 5
AI/BattleAI/BattleAI.cpp

@@ -259,11 +259,9 @@ void CBattleAI::attemptCastingSpell()
 			{
 				StackWithBonuses swb;
 				swb.stack = sta;
-				Bonus pseudoBonus;
-				pseudoBonus.sid = ps.spell->id;
-				pseudoBonus.val = skillLevel;
-				pseudoBonus.turnsRemain = 1; //TODO
-				CStack::stackEffectToFeature(swb.bonusesToAdd, pseudoBonus);
+				//todo: handle effect actualization in HypotheticChangesToBattleState
+				ps.spell->getEffects(swb.bonusesToAdd, skillLevel, false, hero->getEnchantPower(ps.spell));
+				ps.spell->getEffects(swb.bonusesToAdd, skillLevel, true, hero->getEnchantPower(ps.spell));
 				HypotheticChangesToBattleState state;
 				state.bonusesOfStacks[swb.stack] = &swb;
 				PotentialTargets pt(swb.stack, state);

+ 3 - 3
AI/VCAI/VCAI.cpp

@@ -746,7 +746,7 @@ void VCAI::makeTurn()
 	logGlobal->info("Player %d (%s) starting turn", playerID, playerID.getStr());
 
 	MAKING_TURN;
-	boost::shared_lock<boost::shared_mutex> gsLock(cb->getGsMutex());
+	boost::shared_lock<boost::shared_mutex> gsLock(CGameState::mutex);
 	setThreadName("VCAI::makeTurn");
 
 	switch(cb->getDate(Date::DAY_OF_WEEK))
@@ -1688,7 +1688,7 @@ void VCAI::battleEnd(const BattleResult *br)
 
 void VCAI::waitTillFree()
 {
-	auto unlock = vstd::makeUnlockSharedGuard(cb->getGsMutex());
+	auto unlock = vstd::makeUnlockSharedGuard(CGameState::mutex);
 	status.waitTillFree();
 }
 
@@ -2787,7 +2787,7 @@ void VCAI::requestActionASAP(std::function<void()> whatToDo)
 	{
 		setThreadName("VCAI::requestActionASAP::whatToDo");
 		SET_GLOBAL_STATE(this);
-		boost::shared_lock<boost::shared_mutex> gsLock(cb->getGsMutex());
+		boost::shared_lock<boost::shared_mutex> gsLock(CGameState::mutex);
 		whatToDo();
 	});
 }

+ 1 - 1
CCallback.cpp

@@ -180,7 +180,7 @@ int CBattleCallback::sendRequest(const CPack *request)
 	if(waitTillRealize)
 	{
 		logGlobal->traceStream() << boost::format("We'll wait till request %d is answered.\n") % requestID;
-		auto gsUnlocker = vstd::makeUnlockSharedGuardIf(getGsMutex(), unlockGsWhenWaiting);
+		auto gsUnlocker = vstd::makeUnlockSharedGuardIf(CGameState::mutex, unlockGsWhenWaiting);
 		cl->waitingRequest.waitWhileContains(requestID);
 	}
 

BIN
CI/deploy_rsa.enc


+ 16 - 0
CI/get_package_name.sh

@@ -0,0 +1,16 @@
+#!/bin/sh
+VCMI_PACKAGE_FILE_NAME="${TRAVIS_JOB_ID}-vcmi"
+VCMI_PACKAGE_NAME_SUFFIX=""
+if [ "$TRAVIS_PULL_REQUEST" = "false" ];
+then
+	branch_name=$(echo "$TRAVIS_BRANCH" | sed 's/[^[:alnum:]]\+/_/g')
+	VCMI_PACKAGE_FILE_NAME="${VCMI_PACKAGE_FILE_NAME}-branch-${branch_name}-${TRAVIS_COMMIT}"
+	VCMI_PACKAGE_NAME_SUFFIX="branch ${branch_name}"
+else
+	VCMI_PACKAGE_FILE_NAME="${VCMI_PACKAGE_FILE_NAME}-PR-${TRAVIS_PULL_REQUEST}-${TRAVIS_COMMIT}"
+	VCMI_PACKAGE_NAME_SUFFIX="PR ${TRAVIS_PULL_REQUEST}"
+fi
+VCMI_PACKAGE_NAME_SUFFIX="(${VCMI_PACKAGE_NAME_SUFFIX})"
+
+export VCMI_PACKAGE_FILE_NAME
+export VCMI_PACKAGE_NAME_SUFFIX

+ 0 - 0
.travis.linux → CI/linux/before_install.sh


+ 1 - 0
CI/linux/upload_package.sh

@@ -0,0 +1 @@
+#!/bin/sh

+ 1 - 1
.travis.osx → CI/mac/before_install.sh

@@ -14,4 +14,4 @@ wget https://github.com/sparkle-project/Sparkle/releases/download/$sparkle_versi
 mkdir sparkle && cd sparkle
 tar -xf ../Sparkle-*.tar.bz2
 sudo mv Sparkle.framework /Library/Frameworks/
-cd .. && rm -rf sparkle
+cd .. && rm -rf sparkle

+ 10 - 0
CI/mac/upload_package.sh

@@ -0,0 +1,10 @@
+#!/bin/sh
+cpack
+
+touch /tmp/deploy_rsa
+chmod 600 /tmp/deploy_rsa
+openssl aes-256-cbc -K $encrypted_1d30f79f8582_key -iv $encrypted_1d30f79f8582_iv -in $TRAVIS_BUILD_DIR/CI/deploy_rsa.enc -out /tmp/deploy_rsa -d
+eval "$(ssh-agent -s)"
+ssh-add /tmp/deploy_rsa
+
+sftp -r -o StrictHostKeyChecking=no [email protected] <<< "put $VCMI_PACKAGE_FILE_NAME.dmg /incoming/$VCMI_PACKAGE_FILE_NAME.dmg"

+ 3 - 0
.travis.mxe → CI/mxe/before_install.sh

@@ -22,6 +22,9 @@ mxe-$MXE_TARGET-ffmpeg \
 mxe-$MXE_TARGET-qt \
 mxe-$MXE_TARGET-qtbase
 
+# Install nsis for installer creation
+sudo apt-get install -qq nsis
+
 # alias for CMake
 sudo mv /usr/bin/cmake /usr/bin/cmake.orig
 sudo ln -s /usr/lib/mxe/usr/bin/$MXE_TARGET-cmake /usr/bin/cmake

+ 10 - 0
CI/mxe/upload_package.sh

@@ -0,0 +1,10 @@
+#!/bin/sh
+cpack
+
+touch /tmp/deploy_rsa
+chmod 600 /tmp/deploy_rsa
+openssl aes-256-cbc -K $encrypted_1d30f79f8582_key -iv $encrypted_1d30f79f8582_iv -in $TRAVIS_BUILD_DIR/CI/deploy_rsa.enc -out /tmp/deploy_rsa -d
+eval "$(ssh-agent -s)"
+ssh-add /tmp/deploy_rsa
+
+sftp -r -o StrictHostKeyChecking=no [email protected] <<< "put $VCMI_PACKAGE_FILE_NAME.exe /incoming/$VCMI_PACKAGE_FILE_NAME.exe"

+ 33 - 12
CMakeLists.txt

@@ -16,6 +16,10 @@ set(VCMI_VERSION_MAJOR 0)
 set(VCMI_VERSION_MINOR 99)
 set(VCMI_VERSION_PATCH 0)
 
+# Allow to pass package name from Travis CI
+set(PACKAGE_NAME_SUFFIX "" CACHE STRING "Suffix for CPack package name")
+set(PACKAGE_FILE_NAME "" CACHE STRING "Override for CPack package filename")
+
 option(ENABLE_ERM "Enable compilation of ERM scripting module" OFF)
 option(ENABLE_EDITOR "Enable compilation of map editor" OFF)
 option(ENABLE_LAUNCHER "Enable compilation of launcher" ON)
@@ -238,14 +242,17 @@ endif()
 # For apple these files will be already inside vcmiclient bundle
 if (NOT APPLE)
 	# copy whole directory but .svn control files
-	install(DIRECTORY config DESTINATION ${DATA_DIR} PATTERN ".svn" EXCLUDE)
+	install(DIRECTORY config DESTINATION ${DATA_DIR})
 	# copy vcmi mod along with all its content
-	install(DIRECTORY Mods/vcmi DESTINATION ${DATA_DIR}/Mods PATTERN ".svn" EXCLUDE)
-
-	install(FILES vcmibuilder DESTINATION ${BIN_DIR} PERMISSIONS
-		OWNER_WRITE OWNER_READ OWNER_EXECUTE
-		            GROUP_READ GROUP_EXECUTE
-		            WORLD_READ WORLD_EXECUTE)
+	install(DIRECTORY Mods/vcmi DESTINATION ${DATA_DIR}/Mods)
+
+	# that script is useless for Windows
+	if (NOT WIN32)
+		install(FILES vcmibuilder DESTINATION ${BIN_DIR} PERMISSIONS
+			OWNER_WRITE OWNER_READ OWNER_EXECUTE
+						GROUP_READ GROUP_EXECUTE
+						WORLD_READ WORLD_EXECUTE)			
+	endif()
 endif()
 
 if(WIN32)
@@ -317,15 +324,28 @@ set(CPACK_PACKAGE_VERSION "${CPACK_PACKAGE_VERSION_MAJOR}.${CPACK_PACKAGE_VERSIO
 
 set(CPACK_INCLUDE_TOPLEVEL_DIRECTORY 0)
 
+if("${PACKAGE_NAME_SUFFIX}" STREQUAL "")
+	set(CPACK_PACKAGE_NAME "VCMI")
+else()
+	set(CPACK_PACKAGE_NAME "VCMI ${PACKAGE_NAME_SUFFIX}")
+endif()
+if("${PACKAGE_FILE_NAME}" STREQUAL "")
+	set(CPACK_PACKAGE_FILE_NAME "vcmi-${CPACK_PACKAGE_VERSION}")
+else()
+	set(CPACK_PACKAGE_FILE_NAME "${PACKAGE_FILE_NAME}")
+endif()
+set(CPACK_PACKAGE_VENDOR "VCMI team")
+
 if(WIN32)
 	set(CPACK_MONOLITHIC_INSTALL 1)
-	set(CPACK_PACKAGE_NAME "VCMI")
-	set(CPACK_PACKAGE_VENDOR "VCMI team")
-	set(CPACK_PACKAGE_FILE_NAME "vcmi-${CPACK_PACKAGE_VERSION}-win32")
 	set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_CURRENT_SOURCE_DIR}/license.txt")
 	set(CPACK_PACKAGE_EXECUTABLES "VCMI_launcher;VCMI")
 	set(CPACK_PACKAGE_INSTALL_DIRECTORY "${CPACK_PACKAGE_NAME}")
-	set(CPACK_NSIS_PACKAGE_NAME "VCMI ${CPACK_PACKAGE_VERSION}")
+	if("${PACKAGE_NAME_SUFFIX}" STREQUAL "")
+		set(CPACK_NSIS_PACKAGE_NAME "VCMI ${CPACK_PACKAGE_VERSION}")
+	else()
+		set(CPACK_NSIS_PACKAGE_NAME "VCMI ${CPACK_PACKAGE_VERSION} ${PACKAGE_NAME_SUFFIX} ")
+	endif()
 	set(CPACK_NSIS_INSTALL_ROOT "$PROGRAMFILES")
 	set(CPACK_NSIS_EXTRA_INSTALL_COMMANDS " CreateShortCut \\\"$DESKTOP\\\\VCMI.lnk\\\" \\\"$INSTDIR\\\\VCMI_launcher.exe\\\"")
     set(CPACK_NSIS_EXTRA_UNINSTALL_COMMANDS " Delete \\\"$DESKTOP\\\\VCMI.lnk\\\" ")
@@ -340,8 +360,9 @@ else()
 	set(CPACK_GENERATOR TGZ)
 endif()
 
-INCLUDE(CPack)
 
 include(GetGitRevisionDescription)
 get_git_head_revision(GIT_REFSPEC GIT_SHA1)
 configure_file("${CMAKE_CURRENT_SOURCE_DIR}/Version.cpp.in" "${CMAKE_BINARY_DIR}/Version.cpp" @ONLY)
+
+INCLUDE(CPack)

+ 3 - 0
ChangeLog

@@ -3,6 +3,9 @@
 GENERAL:
 * Spectator mode was implemented through command-line options
 
+SPELLS:
+* Implemented cumulative effects for spells
+
 0.98 -> 0.99
 
 GENERAL:

+ 8 - 11
client/CPlayerInterface.cpp

@@ -123,7 +123,6 @@ CPlayerInterface::CPlayerInterface(PlayerColor Player)
 	currentSelection = nullptr;
 	castleInt = nullptr;
 	battleInt = nullptr;
-	//pim = new boost::recursive_mutex;
 	makingTurn = false;
 	showingDialog = new CondSh<bool>(false);
 	cingconsole = new CInGameConsole;
@@ -140,8 +139,6 @@ CPlayerInterface::~CPlayerInterface()
 {
 	logGlobal->traceStream() << "\tHuman player interface for player " << playerID << " being destructed";
 	//howManyPeople--;
-	//delete pim;
-	//vstd::clear_pointer(pim);
 	delete showingDialog;
 	delete cingconsole;
 	if (LOCPLINT == this)
@@ -821,10 +818,10 @@ BattleAction CPlayerInterface::activeStack(const CStack * stack) //called when i
 
 	CBattleInterface *b = battleInt;
 
-	if (b->givenCommand->get())
+	if(CBattleInterface::givenCommand.get())
 	{
 		logGlobal->errorStream() << "Command buffer must be clean! (we don't want to use old command)";
-		vstd::clear_pointer(b->givenCommand->data);
+		vstd::clear_pointer(CBattleInterface::givenCommand.data);
 	}
 
 	{
@@ -833,17 +830,17 @@ BattleAction CPlayerInterface::activeStack(const CStack * stack) //called when i
 		//Regeneration & mana drain go there
 	}
 	//wait till BattleInterface sets its command
-	boost::unique_lock<boost::mutex> lock(b->givenCommand->mx);
-	while(!b->givenCommand->data)
+	boost::unique_lock<boost::mutex> lock(CBattleInterface::givenCommand.mx);
+	while(!CBattleInterface::givenCommand.data)
 	{
-		b->givenCommand->cond.wait(lock);
+		CBattleInterface::givenCommand.cond.wait(lock);
 		if (!battleInt) //battle ended while we were waiting for movement (eg. because of spell)
 			throw boost::thread_interrupted(); //will shut the thread peacefully
 	}
 
 	//tidy up
-	BattleAction ret = *(b->givenCommand->data);
-	vstd::clear_pointer(b->givenCommand->data);
+	BattleAction ret = *(CBattleInterface::givenCommand.data);
+	vstd::clear_pointer(CBattleInterface::givenCommand.data);
 
 	if (ret.actionType == Battle::CANCEL)
 	{
@@ -1637,7 +1634,7 @@ void CPlayerInterface::setSelection(const CArmedInstance * obj)
 void CPlayerInterface::update()
 {
 	// Make sure that gamestate won't change when GUI objects may obtain its parts on event processing or drawing request
-	boost::shared_lock<boost::shared_mutex> gsLock(cb->getGsMutex());
+	boost::shared_lock<boost::shared_mutex> gsLock(CGameState::mutex);
 
 	// While mutexes were locked away we may be have stopped being the active interface
 	if (LOCPLINT != this)

+ 18 - 27
client/battle/CBattleInterface.cpp

@@ -47,6 +47,7 @@
  */
 
 CondSh<bool> CBattleInterface::animsAreDisplayed(false);
+CondSh<BattleAction *> CBattleInterface::givenCommand(nullptr);
 
 static void onAnimationFinished(const CStack *stack, CCreatureAnimation *anim)
 {
@@ -101,7 +102,7 @@ CBattleInterface::CBattleInterface(const CCreatureSet *army1, const CCreatureSet
 	  currentlyHoveredHex(-1), attackingHex(-1), stackCanCastSpell(false), creatureCasting(false), spellDestSelectMode(false), spellToCast(nullptr), sp(nullptr),
 	  creatureSpellToCast(-1),
 	  siegeH(nullptr), attackerInt(att), defenderInt(defen), curInt(att), animIDhelper(0),
-	  givenCommand(nullptr), myTurn(false), resWindow(nullptr), moveStarted(false), moveSoundHander(-1), bresult(nullptr)
+	  myTurn(false), resWindow(nullptr), moveStarted(false), moveSoundHander(-1), bresult(nullptr)
 {
 	OBJ_CONSTRUCTION;
 
@@ -117,7 +118,7 @@ CBattleInterface::CBattleInterface(const CCreatureSet *army1, const CCreatureSet
 	animsAreDisplayed.setn(false);
 	pos = myRect;
 	strongInterest = true;
-	givenCommand = new CondSh<BattleAction *>(nullptr);
+	givenCommand.setn(nullptr);
 
 	//hot-seat -> check tactics for both players (defender may be local human)
 	if (attackerInt && attackerInt->cb->battleGetTacticDist())
@@ -387,8 +388,7 @@ CBattleInterface::CBattleInterface(const CCreatureSet *army1, const CCreatureSet
 CBattleInterface::~CBattleInterface()
 {
 	curInt->battleInt = nullptr;
-	givenCommand->cond.notify_all(); //that two lines should make any activeStack waiting thread to finish
-
+	givenCommand.cond.notify_all(); //that two lines should make any activeStack waiting thread to finish
 
 	if (active) //dirty fix for #485
 	{
@@ -416,7 +416,6 @@ CBattleInterface::~CBattleInterface()
 	delete bConsoleUp;
 	delete bConsoleDown;
 	delete console;
-	delete givenCommand;
 
 	delete attackingHero;
 	delete defendingHero;
@@ -1028,7 +1027,7 @@ void CBattleInterface::stackRemoved(int stackID)
 			action->side = defendingHeroInstance ? (curInt->playerID == defendingHeroInstance->tempOwner) : false;
 			action->actionType = Battle::CANCEL;
 			action->stackNumber = activeStack->ID;
-			givenCommand->setn(action);
+			givenCommand.setn(action);
 			setActiveStack(nullptr);
 		}
 	}
@@ -1149,7 +1148,7 @@ void CBattleInterface::giveCommand(Battle::ActionType action, BattleHex tile, ui
 		logGlobal->traceStream() << "Setting command for " << (stack ? stack->nodeName() : "hero");
 		myTurn = false;
 		setActiveStack(nullptr);
-		givenCommand->setn(ba);
+		givenCommand.setn(ba);
 	}
 	else
 	{
@@ -1359,52 +1358,44 @@ void CBattleInterface::spellCast(const BattleSpellCast *sc)
 
 void CBattleInterface::battleStacksEffectsSet(const SetStackEffect & sse)
 {
-	if (sse.effect.back().sid == -1 && sse.stacks.size() == 1 && sse.effect.size() == 2)
+	if(sse.stacks.size() == 1 && sse.effect.size() == 2 && sse.effect.back().sid == -1)
 	{
 		const Bonus & bns = sse.effect.front();
-		if (bns.source == Bonus::OTHER && bns.type == Bonus::PRIMARY_SKILL)
+		if(bns.source == Bonus::OTHER && bns.type == Bonus::PRIMARY_SKILL)
 		{
 			//defensive stance
 			const CStack *stack = LOCPLINT->cb->battleGetStackByID(*sse.stacks.begin());
 			int txtid = 120;
 
-			if (stack->count != 1)
+			if(stack->count != 1)
 				txtid++; //move to plural text
 
 			BonusList defenseBonuses = *(stack->getBonuses(Selector::typeSubtype(Bonus::PRIMARY_SKILL, PrimarySkill::DEFENSE)));
-			defenseBonuses.remove_if (Bonus::UntilGetsTurn); //remove bonuses gained from defensive stance
+			defenseBonuses.remove_if(Bonus::UntilGetsTurn); //remove bonuses gained from defensive stance
 			int val = stack->Defense() - defenseBonuses.totalValue();
-			auto txt = boost::format (CGI->generaltexth->allTexts[txtid]) % ((stack->count != 1) ? stack->getCreature()->namePl : stack->getCreature()->nameSing) % val;
+			auto txt = boost::format(CGI->generaltexth->allTexts[txtid]) % ((stack->count != 1) ? stack->getCreature()->namePl : stack->getCreature()->nameSing) % val;
 			console->addText(boost::to_string(txt));
 		}
 	}
 
-	if (activeStack != nullptr) //it can be -1 when a creature casts effect
-	{
+	if(activeStack != nullptr)
 		redrawBackgroundWithHexes(activeStack);
-	}
 }
 
-CBattleInterface::PossibleActions CBattleInterface::getCasterAction(const CSpell *spell, const ISpellCaster *caster, ECastingMode::ECastingMode mode) const
+CBattleInterface::PossibleActions CBattleInterface::getCasterAction(const CSpell * spell, const ISpellCaster * caster, ECastingMode::ECastingMode mode) const
 {
 	PossibleActions spellSelMode = ANY_LOCATION;
 
 	const CSpell::TargetInfo ti(spell, caster->getSpellSchoolLevel(spell), mode);
 
-	if (ti.massive || ti.type == CSpell::NO_TARGET)
+	if(ti.massive || ti.type == CSpell::NO_TARGET)
 		spellSelMode = NO_LOCATION;
-	else if (ti.type == CSpell::LOCATION && ti.clearAffected)
-	{
+	else if(ti.type == CSpell::LOCATION && ti.clearAffected)
 		spellSelMode = FREE_LOCATION;
-	}
-	else if (ti.type == CSpell::CREATURE)
-	{
+	else if(ti.type == CSpell::CREATURE)
 		spellSelMode = AIMED_SPELL_CREATURE;
-	}
-	else if (ti.type == CSpell::OBSTACLE)
-	{
+	else if(ti.type == CSpell::OBSTACLE)
 		spellSelMode = OBSTACLE;
-	}
 
 	return spellSelMode;
 }
@@ -2848,7 +2839,7 @@ void CBattleInterface::requestAutofightingAIToTakeAction()
 			}
 			else
 			{
-				givenCommand->setn(ba.release());
+				givenCommand.setn(ba.release());
 			}
 		}
 		else

+ 5 - 2
client/battle/CBattleInterface.h

@@ -264,10 +264,13 @@ private:
 
 	PossibleActions getCasterAction(const CSpell *spell, const ISpellCaster *caster, ECastingMode::ECastingMode mode) const;
 public:
+	static CondSh<bool> animsAreDisplayed; //for waiting with the end of battle for end of anims
+	static CondSh<BattleAction *> givenCommand; //data != nullptr if we have i.e. moved current unit
+
 	std::list<std::pair<CBattleAnimation *, bool>> pendingAnims; //currently displayed animations <anim, initialized>
 	void addNewAnim(CBattleAnimation *anim); //adds new anim to pendingAnims
 	ui32 animIDhelper; //for giving IDs for animations
-	static CondSh<bool> animsAreDisplayed; //for waiting with the end of battle for end of anims
+
 
 	CBattleInterface(const CCreatureSet *army1, const CCreatureSet *army2, const CGHeroInstance *hero1, const CGHeroInstance *hero2, const SDL_Rect & myRect, std::shared_ptr<CPlayerInterface> att, std::shared_ptr<CPlayerInterface> defen, std::shared_ptr<CPlayerInterface> spectatorInt = nullptr); //c-tor
 	virtual ~CBattleInterface(); //d-tor
@@ -282,7 +285,7 @@ public:
 
 	std::vector<CClickableHex*> bfield; //11 lines, 17 hexes on each
 	SDL_Surface *cellBorder, *cellShade;
-	CondSh<BattleAction *> *givenCommand; //data != nullptr if we have i.e. moved current unit
+
 	bool myTurn; //if true, interface is active (commands can be ordered)
 	CBattleResultWindow *resWindow; //window of end of battle
 

+ 26 - 19
config/schemas/spell.json

@@ -19,25 +19,25 @@
 					{
 						//assumed verticalPosition: top
 						"type": "string",
-						"format": "defFile"				
+						"format": "defFile"
 					},
 					{
 						"type": "object",
 						"properties":{
 							"verticalPosition": {"type":"string", "enum":["top","bottom"]},
 							"defName": {"type":"string", "format": "defFile"}
-						},					
-						"additionalProperties" : false				
+						},
+						"additionalProperties" : false
 					}
-				]					
-			}			
+				]
+			}
 		},
 		"animation":{
 			"type": "object",
 			"additionalProperties" : false,
 			"properties":{
-				"affect":{"$ref" : "#/definitions/animationQueue"},				
-				"hit":{"$ref" : "#/definitions/animationQueue"},				
+				"affect":{"$ref" : "#/definitions/animationQueue"},
+				"hit":{"$ref" : "#/definitions/animationQueue"},
 				"cast":{"$ref" : "#/definitions/animationQueue"},
 				"projectile":{
 					"type":"array",
@@ -46,11 +46,11 @@
 						"properties":{
 							"minimumAngle": {"type":"number", "minimum" : 0},
 							"defName": {"type":"string", "format": "defFile"}
-						},					
-						"additionalProperties" : false						
+						},
+						"additionalProperties" : false
 					}
-				}				
-			}		
+				}
+			}
 		},
 		"flags" :{
 			"type" : "object",
@@ -85,7 +85,14 @@
 				},
 				"effects":{
 					"type": "object",
-					"description": "Timed effects",
+					"description": "Timed effects (updated by prolongation)",
+					"additionalProperties" : {
+						"$ref" : "vcmi:bonus"
+					}
+				},
+				"cumulativeEffects":{
+					"type": "object",
+					"description": "Timed effects (updated by unique bonus)",
 					"additionalProperties" : {
 						"$ref" : "vcmi:bonus"
 					}
@@ -107,16 +114,16 @@
 						{
 							"type": "boolean",
 							"description": "LOCATION target only. All affected hexes/tile must be clear"
-						}						
+						}
 					}
 				}
 			}
 		},
-		
+
 		"texts":{
 			"type": "object",
-			
-			
+
+
 			"additionalProperties" : false
 		}
 	},
@@ -239,9 +246,9 @@
 				 "$ref" : "#/definitions/flags",
 				 "description": "flags structure of bonus names, presence of all bonuses required to be affected by, can't be negated."
 		},
-		
+
 		"animation":{"$ref": "#/definitions/animation"},
-		
+
 		"graphics":{
 				 "type": "object",
 				 "additionalProperties" : false,
@@ -291,7 +298,7 @@
 			 "additionalProperties" : false,
 			 "required" : ["none", "basic", "advanced", "expert"],
 
-			 "properties":{	
+			 "properties":{
 				"base":{
 					"type": "object",
 					"description": "will be merged with all levels",

+ 2 - 1
config/spells/ability.json

@@ -101,6 +101,7 @@
 		"levels" : {
 			"base":{
 				"range" : "0",
+				//no cumulative effect even with mods here
 				"effects" : {
 					"bindEffect" : {
 						"val" : 0,
@@ -335,7 +336,7 @@
 			"base":{
 				"range" : "0",
 				"targetModifier":{"smart":true},
-				"effects" : {
+				"cumulativeEffects" : {
 					"primarySkill" : {
 						"val" : -3,
 						"type" : "PRIMARY_SKILL",

+ 5 - 3
config/spells/timed.json

@@ -13,6 +13,7 @@
 			"base":{
 				"range" : "0",
 				"targetModifier":{"smart":true},
+				//no cumulative effect even with mods here
 				"effects" : {
 					"generalDamageReduction" : {
 						"type" : "GENERAL_DAMAGE_REDUCTION",
@@ -43,6 +44,7 @@
 			"base":{
 				"range" : "0",
 				"targetModifier":{"smart":true},
+				//no cumulative effect even with mods here
 				"effects" : {
 					"generalDamageReduction" : {
 						"type" : "GENERAL_DAMAGE_REDUCTION",
@@ -558,7 +560,7 @@
 			"base":{
 				"range" : "0",
 				"targetModifier":{"smart":true},
-				"effects" : {
+				"cumulativeEffects" : {
 					"primarySkill" : {
 						"type" : "PRIMARY_SKILL",
 						"subtype" : "primSkill.defence",
@@ -569,14 +571,14 @@
 				}
 			},
 			"advanced":{
-				"effects" : {
+				"cumulativeEffects" : {
 					"primarySkill" : {
 						"val" : -4
 					}
 				}
 			},
 			"expert":{
-				"effects" : {
+				"cumulativeEffects" : {
 					"primarySkill" : {
 						"val" : -5
 					}

+ 4 - 2
lib/CArtHandler.cpp

@@ -130,7 +130,7 @@ void CArtifact::addNewBonus(const std::shared_ptr<Bonus>& b)
 
 void CArtifact::fillWarMachine()
 {
-	switch (id)
+	switch(id)
 	{
 	case ArtifactID::CATAPULT:
 		warMachine = CreatureID::CATAPULT;
@@ -144,8 +144,10 @@ void CArtifact::fillWarMachine()
 	case ArtifactID::AMMO_CART:
 		warMachine = CreatureID::AMMO_CART;
 		break;
+	default:
+		warMachine = CreatureID::NONE;
+		break;
 	}
-	warMachine = CreatureID::NONE; //this artifact is not a creature
 }
 
 void CGrowingArtifact::levelUpArtifact (CArtifactInstance * art)

+ 0 - 5
lib/CBattleCallback.cpp

@@ -100,11 +100,6 @@ namespace SiegeStuffThatShouldBeMovedToHandlers //  <=== TODO
 
 using namespace SiegeStuffThatShouldBeMovedToHandlers;
 
-boost::shared_mutex& CCallbackBase::getGsMutex()
-{
-	return *gs->mx;
-}
-
 bool CCallbackBase::duringBattle() const
 {
 	return getBattle() != nullptr;

+ 0 - 1
lib/CBattleCallback.h

@@ -62,7 +62,6 @@ protected:
 	bool duringBattle() const;
 
 public:
-	boost::shared_mutex &getGsMutex(); //just return a reference to mutex, does not lock nor anything
 	boost::optional<PlayerColor> getPlayerID() const;
 
 	friend class CBattleInfoEssentials;

+ 3 - 3
lib/CGameState.cpp

@@ -45,6 +45,8 @@
  *
  */
 
+boost::shared_mutex CGameState::mutex;
+
 template <typename T> class CApplyOnGS;
 
 class CBaseForGSApply
@@ -65,7 +67,7 @@ public:
 	{
 		T *ptr = static_cast<T*>(pack);
 
-		boost::unique_lock<boost::shared_mutex> lock(*gs->mx);
+		boost::unique_lock<boost::shared_mutex> lock(CGameState::mutex);
 		ptr->applyGs(gs);
 	}
 };
@@ -675,7 +677,6 @@ int CGameState::getDate(Date::EDateType mode) const
 CGameState::CGameState()
 {
 	gs = this;
-	mx = new boost::shared_mutex();
 	applierGs = new CApplier<CBaseForGSApply>;
 	registerTypesClientPacks1(*applierGs);
 	registerTypesClientPacks2(*applierGs);
@@ -687,7 +688,6 @@ CGameState::CGameState()
 
 CGameState::~CGameState()
 {
-	//delete mx;//TODO: crash on Linux (mutex must be unlocked before destruction)
 	map.dellNull();
 	curB.dellNull();
 	//delete scenarioOps; //TODO: fix for loading ind delete

+ 1 - 1
lib/CGameState.h

@@ -213,7 +213,7 @@ public:
 	CBonusSystemNode globalEffects;
 	RumorState rumor;
 
-	boost::shared_mutex *mx;
+	static boost::shared_mutex mutex;
 
 	void giveHeroArtifact(CGHeroInstance *h, ArtifactID aid);
 

+ 0 - 15
lib/CStack.cpp

@@ -104,21 +104,6 @@ si32 CStack::magicResistance() const
 	return magicResistance;
 }
 
-void CStack::stackEffectToFeature(std::vector<Bonus> & sf, const Bonus & sse)
-{
-	const CSpell * sp = SpellID(sse.sid).toSpell();
-
-	std::vector<Bonus> tmp;
-	sp->getEffects(tmp, sse.val);
-
-	for(Bonus& b : tmp)
-	{
-		if(b.turnsRemain == 0)
-			b.turnsRemain = sse.turnsRemain;
-		sf.push_back(b);
-	}
-}
-
 bool CStack::willMove(int turn /*= 0*/) const
 {
 	return ( turn ? true : !vstd::contains(state, EBattleStackState::DEFENDING) )

+ 0 - 1
lib/CStack.h

@@ -62,7 +62,6 @@ public:
 	ui32 calculateHealedHealthPoints(ui32 toHeal, const bool resurrect) const;
 	ui32 level() const;
 	si32 magicResistance() const override; //include aura of resistance
-	static void stackEffectToFeature(std::vector<Bonus> & sf, const Bonus & sse);
 	std::vector<si32> activeSpells() const; //returns vector of active spell IDs sorted by time of cast
 	const CGHeroInstance *getMyHero() const; //if stack belongs to hero (directly or was by him summoned) returns hero, nullptr otherwise
 	ui32 totalHealth() const; // total health for all creatures in stack;

+ 8 - 0
lib/NetPacks.h

@@ -1519,11 +1519,19 @@ struct SetStackEffect : public CPackForClient
 	void applyCl(CClient *cl);
 
 	std::vector<ui32> stacks; //affected stacks (IDs)
+
+	//regular effects
 	std::vector<Bonus> effect; //bonuses to apply
 	std::vector<std::pair<ui32, Bonus> > uniqueBonuses; //bonuses per single stack
+
+	//cumulative effects
+	std::vector<Bonus> cumulativeEffects; //bonuses to apply
+	std::vector<std::pair<ui32, Bonus> > cumulativeUniqueBonuses; //bonuses per single stack
+
 	template <typename Handler> void serialize(Handler &h, const int version)
 	{
 		h & stacks & effect & uniqueBonuses;
+		h & cumulativeEffects & cumulativeUniqueBonuses;
 	}
 };
 

+ 22 - 7
lib/NetPacksLib.cpp

@@ -1554,33 +1554,39 @@ void actualizeEffect(CStack * s, const std::vector<Bonus> & ef)
 
 DLL_LINKAGE void SetStackEffect::applyGs(CGameState *gs)
 {
-	if(effect.empty())
+	if(effect.empty() && cumulativeEffects.empty())
 	{
 		logGlobal->errorStream() << "Trying to apply SetStackEffect with no effects";
 		return;
 	}
 
-	int spellid = effect.begin()->sid; //effects' source ID
+	si32 spellid = effect.empty() ? cumulativeEffects.begin()->sid : effect.begin()->sid; //effects' source ID
 
-	auto processEffect = [spellid, this](CStack * sta, const Bonus & effect)
+	auto processEffect = [spellid, this](CStack * sta, const Bonus & effect, bool cumulative)
 	{
-		if(!sta->hasBonus(Selector::source(Bonus::SPELL_EFFECT, spellid).And(Selector::typeSubtype(effect.type, effect.subtype)))
-			|| spellid == SpellID::DISRUPTING_RAY || spellid == SpellID::ACID_BREATH_DEFENSE)
+		if(cumulative || !sta->hasBonus(Selector::source(Bonus::SPELL_EFFECT, spellid).And(Selector::typeSubtype(effect.type, effect.subtype))))
 		{
 			//no such effect or cumulative - add new
 			logBonus->traceStream() << sta->nodeName() << " receives a new bonus: " << effect.Description();
 			sta->addNewBonus(std::make_shared<Bonus>(effect));
 		}
 		else
+		{
+			logBonus->traceStream() << sta->nodeName() << " updated bonus: " << effect.Description();
 			actualizeEffect(sta, effect);
+		}
 	};
 
 	for(ui32 id : stacks)
 	{
 		CStack *s = gs->curB->getStack(id);
 		if(s)
+		{
 			for(const Bonus & fromEffect : effect)
-				processEffect(s, fromEffect);
+				processEffect(s, fromEffect, false);
+			for(const Bonus & fromEffect : cumulativeEffects)
+				processEffect(s, fromEffect, true);
+		}
 		else
 			logNetwork->errorStream() << "Cannot find stack " << id;
 	}
@@ -1589,7 +1595,16 @@ DLL_LINKAGE void SetStackEffect::applyGs(CGameState *gs)
 	{
 		CStack *s = gs->curB->getStack(para.first);
 		if(s)
-			processEffect(s, para.second);
+			processEffect(s, para.second, false);
+		else
+			logNetwork->errorStream() << "Cannot find stack " << para.first;
+	}
+
+	for(auto & para : cumulativeUniqueBonuses)
+	{
+		CStack *s = gs->curB->getStack(para.first);
+		if(s)
+			processEffect(s, para.second, true);
 		else
 			logNetwork->errorStream() << "Cannot find stack " << para.first;
 	}

+ 2 - 2
lib/VCMI_lib.vcxproj

@@ -98,7 +98,7 @@
       <AdditionalOptions>/MP4 %(AdditionalOptions) /bigobj
  /Zm150</AdditionalOptions>
       <Optimization>Disabled</Optimization>
-      <AdditionalIncludeDirectories>$(BOOSTDIR); $(ZLIBDIR);$(SDLDIR)</AdditionalIncludeDirectories>
+      <AdditionalIncludeDirectories>$(BOOSTDIR);$(ZLIBDIR);$(SDLDIR)</AdditionalIncludeDirectories>
       <MinimalRebuild>false</MinimalRebuild>
       <BasicRuntimeChecks>EnableFastChecks</BasicRuntimeChecks>
       <RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary>
@@ -139,7 +139,7 @@
       <PreprocessorDefinitions>VCMI_DLL;VCMI_NO_EXTRA_VERSION;%(PreprocessorDefinitions)</PreprocessorDefinitions>
       <PrecompiledHeaderFile>StdInc.h</PrecompiledHeaderFile>
       <PrecompiledHeader>Use</PrecompiledHeader>
-      <AdditionalIncludeDirectories>$(BOOSTDIR); $(ZLIBDIR);$(SDLDIR)</AdditionalIncludeDirectories>
+      <AdditionalIncludeDirectories>$(BOOSTDIR);$(ZLIBDIR);$(SDLDIR)</AdditionalIncludeDirectories>
     </ClCompile>
     <Link>
       <AdditionalDependencies>minizip.lib;zlib.lib;%(AdditionalDependencies)</AdditionalDependencies>

+ 21 - 21
lib/filesystem/CZipSaver.cpp

@@ -7,7 +7,7 @@
  * Full text of license available in license.txt file, in main folder
  *
  */
- 
+
 #include "StdInc.h"
 #include "CZipSaver.h"
 
@@ -17,10 +17,10 @@ CZipOutputStream::CZipOutputStream(CZipSaver * owner_, zipFile archive, const st
 	owner(owner_)
 {
 	zip_fileinfo fileInfo;
-	
+
 	std::time_t t = time(nullptr);
-	fileInfo.dosDate = 0;	
-	
+	fileInfo.dosDate = 0;
+
 	struct tm * localTime = std::localtime(&t);
 	fileInfo.tmz_date.tm_hour = localTime->tm_hour;
 	fileInfo.tmz_date.tm_mday = localTime->tm_mday;
@@ -28,10 +28,10 @@ CZipOutputStream::CZipOutputStream(CZipSaver * owner_, zipFile archive, const st
 	fileInfo.tmz_date.tm_mon  = localTime->tm_mon;
 	fileInfo.tmz_date.tm_sec  = localTime->tm_sec;
 	fileInfo.tmz_date.tm_year = localTime->tm_year;
-		
-	fileInfo.external_fa = 0; //??? 
-	fileInfo.internal_fa = 0;	
-	
+
+	fileInfo.external_fa = 0; //???
+	fileInfo.internal_fa = 0;
+
 	int status = zipOpenNewFileInZip4_64(
 						handle,
 						archiveFilename.c_str(),
@@ -53,10 +53,10 @@ CZipOutputStream::CZipOutputStream(CZipSaver * owner_, zipFile archive, const st
 						0,//flagBase
 						0//zip64
 						);
-    
+
     if(status != ZIP_OK)
 		throw new std::runtime_error("CZipOutputStream: zipOpenNewFileInZip failed");
-	
+
 	owner->activeStream = this;
 }
 
@@ -71,7 +71,7 @@ CZipOutputStream::~CZipOutputStream()
 si64 CZipOutputStream::write(const ui8 * data, si64 size)
 {
 	int ret = zipWriteInFileInZip(handle, (const void*)data, (unsigned)size);
-	
+
 	if (ret == ZIP_OK)
 		return size;
 	else
@@ -79,41 +79,41 @@ si64 CZipOutputStream::write(const ui8 * data, si64 size)
 }
 
 ///CZipSaver
-CZipSaver::CZipSaver(std::shared_ptr<CIOApi> api, const std::string & path):
+CZipSaver::CZipSaver(std::shared_ptr<CIOApi> api, const boost::filesystem::path & path):
 	ioApi(api),
 	zipApi(ioApi->getApiStructure()),
 	handle(nullptr),
 	activeStream(nullptr)
 {
-	handle = zipOpen2_64(path.c_str(), APPEND_STATUS_CREATE, nullptr, &zipApi);
-	
+	handle = zipOpen2_64((const void *) & path, APPEND_STATUS_CREATE, nullptr, &zipApi);
+
 	if (handle == nullptr)
-		throw new std::runtime_error("CZipSaver: Failed to create archive");	
+		throw new std::runtime_error("CZipSaver: Failed to create archive");
 }
 
 CZipSaver::~CZipSaver()
 {
 	if(activeStream != nullptr)
 	{
-		logGlobal->error("CZipSaver::~CZipSaver: active stream found");	
+		logGlobal->error("CZipSaver::~CZipSaver: active stream found");
 		zipCloseFileInZip(handle);
 	}
-		
-	
+
+
 	if(handle != nullptr)
 	{
 		int status = zipClose(handle, nullptr);
 		if (status != ZIP_OK)
-			logGlobal->errorStream() << "CZipSaver: archive finalize failed: "<<status;		
+			logGlobal->errorStream() << "CZipSaver: archive finalize failed: "<<status;
 	}
-		
+
 }
 
 std::unique_ptr<COutputStream> CZipSaver::addFile(const std::string & archiveFilename)
 {
 	if(activeStream != nullptr)
 		throw new std::runtime_error("CZipSaver::addFile: stream already opened");
-	
+
 	std::unique_ptr<COutputStream> stream(new CZipOutputStream(this, handle, archiveFilename));
 	return std::move(stream);
 }

+ 9 - 9
lib/filesystem/CZipSaver.h

@@ -23,16 +23,16 @@ public:
 	 * @brief constructs zip stream from already opened file
 	 * @param archive archive handle, must be opened
 	 * @param archiveFilename name of file to write
-	 */	
+	 */
 	explicit CZipOutputStream(CZipSaver * owner_, zipFile archive, const std::string & archiveFilename);
 	~CZipOutputStream();
-	
+
 	si64 write(const ui8 * data, si64 size) override;
 
 	si64 seek(si64 position) override {return -1;};
 	si64 tell() override {return 0;};
 	si64 skip(si64 delta) override {return 0;};
-	si64 getSize() override {return 0;};	
+	si64 getSize() override {return 0;};
 private:
 	zipFile handle;
 	CZipSaver * owner;
@@ -40,17 +40,17 @@ private:
 
 class DLL_LINKAGE CZipSaver
 {
-public:	
-	explicit CZipSaver(std::shared_ptr<CIOApi> api, const std::string & path);
+public:
+	explicit CZipSaver(std::shared_ptr<CIOApi> api, const boost::filesystem::path & path);
 	virtual ~CZipSaver();
-	
+
 	std::unique_ptr<COutputStream> addFile(const std::string & archiveFilename);
 private:
 	std::shared_ptr<CIOApi> ioApi;
-	zlib_filefunc64_def zipApi;	
-	
+	zlib_filefunc64_def zipApi;
+
 	zipFile handle;
-	
+
 	///due to minizip design only one file stream may opened at a time
 	COutputStream * activeStream;
 	friend class CZipOutputStream;

+ 1 - 1
lib/filesystem/MinizipExtensions.cpp

@@ -204,7 +204,7 @@ zlib_filefunc64_def CProxyROIOApi::getApiStructure()
 
 CInputStream * CProxyROIOApi::openFile(const boost::filesystem::path& filename, int mode)
 {
-	logGlobal->traceStream() << "CProxyIOApi: stream opened for " <<filename.string() <<" with mode "<<mode;
+	logGlobal->traceStream() << "CProxyROIOApi: stream opened for " <<filename.string() <<" with mode "<<mode;
 
 	data->seek(0);
 	return data;

+ 1 - 1
lib/serializer/CSerializer.h

@@ -14,7 +14,7 @@
 #include "../ConstTransitivePtr.h"
 #include "../GameConstants.h"
 
-const ui32 SERIALIZATION_VERSION = 771;
+const ui32 SERIALIZATION_VERSION = 773;
 const ui32 MINIMAL_SERIALIZATION_VERSION = 753;
 const std::string SAVEGAME_MAGIC = "VCMISVG";
 

+ 2 - 1
lib/spells/AdventureSpellMechanics.cpp

@@ -79,11 +79,12 @@ ESpellCastResult AdventureSpellMechanics::applyAdventureEffects(const SpellCastE
 {
 	if(owner->hasEffects())
 	{
+		//todo: cumulative effects support
 		const int schoolLevel = parameters.caster->getSpellSchoolLevel(owner);
 
 		std::vector<Bonus> bonuses;
 
-		owner->getEffects(bonuses, schoolLevel);
+		owner->getEffects(bonuses, schoolLevel, false, parameters.caster->getEnchantPower(owner));
 
 		for(Bonus b : bonuses)
 		{

+ 104 - 91
lib/spells/CDefaultSpellMechanics.cpp

@@ -445,118 +445,131 @@ void DefaultSpellMechanics::battleLogDefault(std::vector<MetaString> & logLines,
 
 void DefaultSpellMechanics::applyBattleEffects(const SpellCastEnvironment * env, const BattleSpellCastParameters & parameters, SpellCastContext & ctx) const
 {
-	//applying effects
 	if(owner->isOffensiveSpell())
-	{
-		const int rawDamage = parameters.getEffectValue();
-		int chainLightningModifier = 0;
-		for(auto & attackedCre : ctx.attackedCres)
-		{
-			BattleStackAttacked bsa;
-			bsa.damageAmount = owner->adjustRawDamage(parameters.caster, attackedCre, rawDamage) >> chainLightningModifier;
-			ctx.addDamageToDisplay(bsa.damageAmount);
+		defaultDamageEffect(env, parameters, ctx);
 
-			bsa.stackAttacked = (attackedCre)->ID;
-			if(parameters.mode == ECastingMode::ENCHANTER_CASTING) //multiple damage spells cast
-				bsa.attackerID = parameters.casterStack->ID;
-			else
-				bsa.attackerID = -1;
-			(attackedCre)->prepareAttacked(bsa, env->getRandomGenerator());
-			ctx.si.stacks.push_back(bsa);
+	if(owner->hasEffects())
+		defaultTimedEffect(env, parameters, ctx);
+}
 
-			if(owner->id == SpellID::CHAIN_LIGHTNING)
-				++chainLightningModifier;
-		}
+void DefaultSpellMechanics::defaultDamageEffect(const SpellCastEnvironment * env, const BattleSpellCastParameters & parameters, SpellCastContext & ctx) const
+{
+	const int rawDamage = parameters.getEffectValue();
+	int chainLightningModifier = 0;
+	for(auto & attackedCre : ctx.attackedCres)
+	{
+		BattleStackAttacked bsa;
+		bsa.damageAmount = owner->adjustRawDamage(parameters.caster, attackedCre, rawDamage) >> chainLightningModifier;
+		ctx.addDamageToDisplay(bsa.damageAmount);
+
+		bsa.stackAttacked = (attackedCre)->ID;
+		if(parameters.mode == ECastingMode::ENCHANTER_CASTING) //multiple damage spells cast
+			bsa.attackerID = parameters.casterStack->ID;
+		else
+			bsa.attackerID = -1;
+		(attackedCre)->prepareAttacked(bsa, env->getRandomGenerator());
+		ctx.si.stacks.push_back(bsa);
+
+		if(owner->id == SpellID::CHAIN_LIGHTNING)
+			++chainLightningModifier;
 	}
+}
 
-	if(owner->hasEffects())
+void DefaultSpellMechanics::defaultTimedEffect(const SpellCastEnvironment * env, const BattleSpellCastParameters & parameters, SpellCastContext & ctx) const
+{
+	SetStackEffect sse;
+	//get default spell duration (spell power with bonuses for heroes)
+	int duration = parameters.enchantPower;
+	//generate actual stack bonuses
 	{
-		SetStackEffect sse;
-		//get default spell duration (spell power with bonuses for heroes)
-		int duration = parameters.enchantPower;
-		//generate actual stack bonuses
-		{
-			int maxDuration = 0;
-			std::vector<Bonus> tmp;
-			owner->getEffects(tmp, parameters.effectLevel);
-			for(Bonus& b : tmp)
-			{
-				//use configured duration if present
-				if(b.turnsRemain == 0)
-					b.turnsRemain = duration;
-				vstd::amax(maxDuration, b.turnsRemain);
-				sse.effect.push_back(b);
-			}
-			//if all spell effects have special duration, use it
-			duration = maxDuration;
-		}
-		//fix to original config: shield should display damage reduction
-		if(owner->id == SpellID::SHIELD || owner->id == SpellID::AIR_SHIELD)
-		{
-			sse.effect.back().val = (100 - sse.effect.back().val);
-		}
-		//we need to know who cast Bind
-		if(owner->id == SpellID::BIND && parameters.casterStack)
-		{
-			sse.effect.back().additionalInfo =  parameters.casterStack->ID;
-		}
-		std::shared_ptr<Bonus> bonus = nullptr;
-		if(parameters.casterHero)
-			bonus = parameters.casterHero->getBonusLocalFirst(Selector::typeSubtype(Bonus::SPECIAL_PECULIAR_ENCHANT, owner->id));
-		//TODO does hero specialty should affects his stack casting spells?
+		si32 maxDuration = 0;
+
+		owner->getEffects(sse.effect, parameters.effectLevel, false, duration, &maxDuration);
+		owner->getEffects(sse.cumulativeEffects, parameters.effectLevel, true, duration, &maxDuration);
+
+		//if all spell effects have special duration, use it later for special bonuses
+		duration = maxDuration;
+	}
+	//fix to original config: shield should display damage reduction
+	if(owner->id == SpellID::SHIELD || owner->id == SpellID::AIR_SHIELD)
+	{
+		sse.effect.at(sse.effect.size() - 1).val = (100 - sse.effect.back().val);
+	}
+	//we need to know who cast Bind
+	else if(owner->id == SpellID::BIND && parameters.casterStack)
+	{
+		sse.effect.at(sse.effect.size() - 1).additionalInfo = parameters.casterStack->ID;
+	}
+	std::shared_ptr<Bonus> bonus = nullptr;
+	if(parameters.casterHero)
+		bonus = parameters.casterHero->getBonusLocalFirst(Selector::typeSubtype(Bonus::SPECIAL_PECULIAR_ENCHANT, owner->id));
+	//TODO does hero specialty should affects his stack casting spells?
 
+	for(const CStack * affected : ctx.attackedCres)
+	{
 		si32 power = 0;
-		for(const CStack * affected : ctx.attackedCres)
-		{
-			sse.stacks.push_back(affected->ID);
+		sse.stacks.push_back(affected->ID);
 
-			//Apply hero specials - peculiar enchants
-			const ui8 tier = std::max((ui8)1, affected->getCreature()->level); //don't divide by 0 for certain creatures (commanders, war machines)
-			if(bonus)
+		//Apply hero specials - peculiar enchants
+		const ui8 tier = std::max((ui8)1, affected->getCreature()->level); //don't divide by 0 for certain creatures (commanders, war machines)
+		if(bonus)
+		{
+			switch(bonus->additionalInfo)
 			{
-				switch(bonus->additionalInfo)
+			case 0: //normal
 				{
-					case 0: //normal
+					switch(tier)
+					{
+					case 1:
+					case 2:
+						power = 3;
+						break;
+					case 3:
+					case 4:
+						power = 2;
+						break;
+					case 5:
+					case 6:
+						power = 1;
+						break;
+					}
+					for(const Bonus & b : sse.effect)
 					{
-						switch(tier)
-						{
-							case 1: case 2:
-								power = 3;
-							break;
-							case 3: case 4:
-								power = 2;
-							break;
-							case 5: case 6:
-								power = 1;
-							break;
-						}
-						Bonus specialBonus(sse.effect.back());
+						Bonus specialBonus(b);
 						specialBonus.val = power; //it doesn't necessarily make sense for some spells, use it wisely
-						sse.uniqueBonuses.push_back (std::pair<ui32,Bonus> (affected->ID, specialBonus)); //additional premy to given effect
+						specialBonus.turnsRemain = duration;
+						sse.uniqueBonuses.push_back(std::pair<ui32, Bonus>(affected->ID, specialBonus)); //additional premy to given effect
 					}
-					break;
-					case 1: //only Coronius as yet
+					for(const Bonus & b : sse.cumulativeEffects)
 					{
-						power = std::max(5 - tier, 0);
-						Bonus specialBonus(Bonus::N_TURNS, Bonus::PRIMARY_SKILL, Bonus::SPELL_EFFECT, power, owner->id, PrimarySkill::ATTACK);
+						Bonus specialBonus(b);
+						specialBonus.val = power; //it doesn't necessarily make sense for some spells, use it wisely
 						specialBonus.turnsRemain = duration;
-						sse.uniqueBonuses.push_back(std::pair<ui32,Bonus> (affected->ID, specialBonus)); //additional attack to Slayer effect
+						sse.cumulativeUniqueBonuses.push_back(std::pair<ui32, Bonus>(affected->ID, specialBonus)); //additional premy to given effect
 					}
-					break;
 				}
-			}
-			if (parameters.casterHero && parameters.casterHero->hasBonusOfType(Bonus::SPECIAL_BLESS_DAMAGE, owner->id)) //TODO: better handling of bonus percentages
-			{
-				int damagePercent = parameters.casterHero->level * parameters.casterHero->valOfBonuses(Bonus::SPECIAL_BLESS_DAMAGE, owner->id.toEnum()) / tier;
-				Bonus specialBonus(Bonus::N_TURNS, Bonus::CREATURE_DAMAGE, Bonus::SPELL_EFFECT, damagePercent, owner->id, 0, Bonus::PERCENT_TO_ALL);
-				specialBonus.turnsRemain = duration;
-				sse.uniqueBonuses.push_back (std::pair<ui32,Bonus> (affected->ID, specialBonus));
+				break;
+			case 1: //only Coronius as yet
+				{
+					power = std::max(5 - tier, 0);
+					Bonus specialBonus(Bonus::N_TURNS, Bonus::PRIMARY_SKILL, Bonus::SPELL_EFFECT, power, owner->id, PrimarySkill::ATTACK);
+					specialBonus.turnsRemain = duration;
+					sse.uniqueBonuses.push_back(std::pair<ui32,Bonus>(affected->ID, specialBonus)); //additional attack to Slayer effect
+				}
+				break;
 			}
 		}
-
-		if(!sse.stacks.empty())
-			env->sendAndApply(&sse);
+		if(parameters.casterHero && parameters.casterHero->hasBonusOfType(Bonus::SPECIAL_BLESS_DAMAGE, owner->id)) //TODO: better handling of bonus percentages
+		{
+			int damagePercent = parameters.casterHero->level * parameters.casterHero->valOfBonuses(Bonus::SPECIAL_BLESS_DAMAGE, owner->id.toEnum()) / tier;
+			Bonus specialBonus(Bonus::N_TURNS, Bonus::CREATURE_DAMAGE, Bonus::SPELL_EFFECT, damagePercent, owner->id, 0, Bonus::PERCENT_TO_ALL);
+			specialBonus.turnsRemain = duration;
+			sse.uniqueBonuses.push_back(std::pair<ui32,Bonus>(affected->ID, specialBonus));
+		}
 	}
+
+	if(!sse.stacks.empty())
+		env->sendAndApply(&sse);
 }
 
 std::vector<BattleHex> DefaultSpellMechanics::rangeInHexes(BattleHex centralHex, ui8 schoolLvl, ui8 side, bool *outDroppedHexes) const

+ 3 - 0
lib/spells/CDefaultSpellMechanics.h

@@ -75,6 +75,9 @@ protected:
 protected:
 	void doDispell(BattleInfo * battle, const BattleSpellCast * packet, const CSelector & selector) const;
 	bool canDispell(const IBonusBearer * obj, const CSelector & selector, const std::string &cachingStr = "") const;
+
+	void defaultDamageEffect(const SpellCastEnvironment * env, const BattleSpellCastParameters & parameters, SpellCastContext & ctx) const;
+	void defaultTimedEffect(const SpellCastEnvironment * env, const BattleSpellCastParameters & parameters, SpellCastContext & ctx) const;
 private:
 	void cast(const SpellCastEnvironment * env, const BattleSpellCastParameters & parameters, std::vector <const CStack*> & reflected) const;
 

+ 36 - 14
lib/spells/CSpellHandler.cpp

@@ -350,7 +350,7 @@ bool CSpell::isSpecialSpell() const
 
 bool CSpell::hasEffects() const
 {
-	return !levels[0].effects.empty();
+	return !levels[0].effects.empty() || !levels[0].cumulativeEffects.empty();
 }
 
 const std::string & CSpell::getIconImmune() const
@@ -382,7 +382,7 @@ si32 CSpell::getProbability(const TFaction factionId) const
 	return probabilities.at(factionId);
 }
 
-void CSpell::getEffects(std::vector<Bonus> & lst, const int level) const
+void CSpell::getEffects(std::vector<Bonus> & lst, const int level, const bool cumulative, const si32 duration, boost::optional<si32 *> maxDuration/* = boost::none*/) const
 {
 	if(level < 0 || level >= GameConstants::SPELL_SCHOOL_LEVELS)
 	{
@@ -390,19 +390,29 @@ void CSpell::getEffects(std::vector<Bonus> & lst, const int level) const
 		return;
 	}
 
-	const std::vector<Bonus> & effects = levels[level].effects;
+	const auto & levelObject = levels.at(level);
 
-	if(effects.empty())
+	if(levelObject.effects.empty() && levelObject.cumulativeEffects.empty())
 	{
 		logGlobal->errorStream() << __FUNCTION__ << " This spell ("  + name + ") has no effects for level " << level;
 		return;
 	}
 
+	const auto & effects = cumulative ? levelObject.cumulativeEffects : levelObject.effects;
+
 	lst.reserve(lst.size() + effects.size());
 
-	for(const Bonus & b : effects)
+	for(const auto b : effects)
 	{
-		lst.push_back(Bonus(b));
+		Bonus nb(*b);
+
+		//use configured duration if present
+		if(nb.turnsRemain == 0)
+			nb.turnsRemain = duration;
+		if(maxDuration)
+			vstd::amax(*(maxDuration.get()), nb.turnsRemain);
+
+		lst.push_back(nb);
 	}
 }
 
@@ -1029,9 +1039,25 @@ CSpell * CSpellHandler::loadFromJson(const JsonNode & json, const std::string &
 			if(usePowerAsValue)
 				b->val = levelPower;
 
-			levelObject.effectsTmp.push_back(b);
+			levelObject.effects.push_back(b);
 		}
 
+		for(const auto & elem : levelNode["cumulativeEffects"].Struct())
+		{
+			const JsonNode & bonusNode = elem.second;
+			auto b = JsonUtils::parseBonus(bonusNode);
+			const bool usePowerAsValue = bonusNode["val"].isNull();
+
+			//TODO: make this work. see CSpellHandler::afterLoadFinalization()
+			//b->sid = spell->id; //for all
+
+			b->source = Bonus::SPELL_EFFECT;//for all
+
+			if(usePowerAsValue)
+				b->val = levelPower;
+
+			levelObject.cumulativeEffects.push_back(b);
+		}
 	}
 
 	return spell;
@@ -1044,14 +1070,10 @@ void CSpellHandler::afterLoadFinalization()
 	{
 		for(auto & level: spell->levels)
 		{
-			for(auto bonus : level.effectsTmp)
-			{
-				level.effects.push_back(*bonus);
-			}
-			level.effectsTmp.clear();
-
 			for(auto & bonus: level.effects)
-				bonus.sid = spell->id;
+				bonus->sid = spell->id;
+			for(auto & bonus: level.cumulativeEffects)
+				bonus->sid = spell->id;
 		}
 		spell->setup();
 	}

+ 31 - 7
lib/spells/CSpellHandler.h

@@ -130,16 +130,35 @@ public:
 		bool clearAffected;
 		std::string range;
 
-		std::vector<Bonus> effects;
-
-		std::vector<std::shared_ptr<Bonus>> effectsTmp; //TODO: this should replace effects
+		std::vector<std::shared_ptr<Bonus>> effects;
+		std::vector<std::shared_ptr<Bonus>> cumulativeEffects;
 
 		LevelInfo();
 		~LevelInfo();
 
 		template <typename Handler> void serialize(Handler &h, const int version)
 		{
-			h & description & cost & power & AIValue & smartTarget & range & effects;
+			h & description & cost & power & AIValue & smartTarget & range;
+
+			if(version >= 773)
+			{
+				h & effects & cumulativeEffects;
+			}
+			else
+			{
+				//all old effects treated as not cumulative, special cases handled by CSpell::serialize
+				std::vector<Bonus> old;
+				h & old;
+
+				if(!h.saving)
+				{
+					effects.clear();
+					cumulativeEffects.clear();
+					for(const Bonus & oldBonus : old)
+						effects.push_back(std::make_shared<Bonus>(oldBonus));
+				}
+			}
+
 			h & clearTarget & clearAffected;
 		}
 	};
@@ -180,7 +199,7 @@ public:
 
 	si32 level;
 
-	std::map<ESpellSchool, bool> school; //todo: use this instead of separate boolean fields
+	std::map<ESpellSchool, bool> school;
 
 	si32 power; //spell's power
 
@@ -215,8 +234,7 @@ public:
 	bool isSpecialSpell() const;
 
 	bool hasEffects() const;
-	void getEffects(std::vector<Bonus> &lst, const int level) const;
-
+	void getEffects(std::vector<Bonus> & lst, const int level, const bool cumulative, const si32 duration, boost::optional<si32 *> maxDuration = boost::none) const;
 
 	///calculate spell damage on stack taking caster`s secondary skills and affectedCreature`s bonuses into account
 	ui32 calculateDamage(const ISpellCaster * caster, const CStack * affectedCreature, int spellSchoolLevel, int usedSpellPower) const;
@@ -264,6 +282,12 @@ public:
 
 		if(!h.saving)
 			setup();
+		//backward compatibility
+		//can not be added to level structure as level structure does not know spell id
+		if(!h.saving && version < 773)
+			if(id == SpellID::DISRUPTING_RAY || id == SpellID::ACID_BREATH_DEFENSE)
+				for(auto & level : levels)
+					std::swap(level.effects, level.cumulativeEffects);
 	}
 	friend class CSpellHandler;
 	friend class Graphics;

+ 31 - 13
server/CGameHandler.cpp

@@ -4489,30 +4489,32 @@ bool CGameHandler::makeCustomAction(BattleAction &ba)
 }
 
 
-void CGameHandler::stackAppearTrigger(const CStack *st)
+void CGameHandler::stackEnchantedTrigger(const CStack * st)
 {
 	auto bl = *(st->getBonuses(Selector::type(Bonus::ENCHANTED)));
-	for (auto b : bl)
+	for(auto b : bl)
 	{
 		SetStackEffect sse;
 		int val = bl.valOfBonuses(Selector::typeSubtype(b->type, b->subtype));
-		if (val > 3)
+		if(val > 3)
 		{
-			for (auto s : gs->curB->battleGetAllStacks())
+			for(auto s : gs->curB->battleGetAllStacks())
 			{
-				if (battleMatchOwner(st, s, true) && s->isValidTarget()) //all allied
+				if(battleMatchOwner(st, s, true) && s->isValidTarget()) //all allied
 					sse.stacks.push_back (s->ID);
 			}
 		}
 		else
 			sse.stacks.push_back (st->ID);
 
-		Bonus pseudoBonus;
-		pseudoBonus.sid = b->subtype;
-		pseudoBonus.val = ((val > 3) ?  (val - 3) : val);
-		pseudoBonus.turnsRemain = 50;
-		st->stackEffectToFeature(sse.effect, pseudoBonus);
-		if (sse.effect.size())
+		const CSpell * sp = SpellID(b->subtype).toSpell();
+		const int level = ((val > 3) ?  (val - 3) : val);
+
+		sp->getEffects(sse.effect, level, false, 50);
+		//this makes effect accumulate for at most 50 turns by default, but effect may be permanent and last till the end of battle
+		sp->getEffects(sse.cumulativeEffects, level, true, 50);
+
+		if(!sse.effect.empty() || !sse.cumulativeEffects.empty())
 			sendAndApply(&sse);
 	}
 }
@@ -4526,7 +4528,6 @@ void CGameHandler::stackTurnTrigger(const CStack *st)
 	bte.additionalInfo = 0;
 	if (st->alive())
 	{
-		stackAppearTrigger(st);
 		//unbind
 		if (st->hasBonus(Selector::type(Bonus::BIND_EFFECT)))
 		{
@@ -5724,7 +5725,7 @@ void CGameHandler::runBattle()
 			}
 		}
 
-		stackAppearTrigger(stack);
+		stackEnchantedTrigger(stack);
 	}
 
 	//spells opening battle
@@ -5749,11 +5750,14 @@ void CGameHandler::runBattle()
 		}
 	}
 
+	bool firstRound = true;//FIXME: why first round is -1?
+
 	//main loop
 	while (!battleResult.get()) //till the end of the battle ;]
 	{
 		BattleNextRound bnr;
 		bnr.round = gs->curB->round + 1;
+		logGlobal->debug("Round %d", bnr.round);
 		sendAndApply(&bnr);
 
 		auto obstacles = gs->curB->obstacles; //we copy container, because we're going to modify it
@@ -5766,6 +5770,12 @@ void CGameHandler::runBattle()
 
 		const BattleInfo & curB = *gs->curB;
 
+		for(auto stack : curB.stacks)
+		{
+			if(stack->alive() && !firstRound)
+				stackEnchantedTrigger(stack);
+		}
+
 		//stack loop
 
 		const CStack *next;
@@ -5994,6 +6004,7 @@ void CGameHandler::runBattle()
 			}
 
 		}
+		firstRound = false;
 	}
 
 	endBattle(gs->curB->tile, gs->curB->battleGetFightingHero(0), gs->curB->battleGetFightingHero(1));
@@ -6207,6 +6218,13 @@ void CGameHandler::handleCheatCode(std::string & cheat, PlayerColor player, cons
 		smp.hid = hero->id;
 		smp.val = 1000000;
 		sendAndApply(&smp);
+
+		GiveBonus gb(GiveBonus::HERO);
+		gb.bonus.type = Bonus::FREE_SHIP_BOARDING;
+		gb.bonus.duration = Bonus::ONE_DAY;
+		gb.bonus.source = Bonus::OTHER;
+		gb.id = hero->id.getNum();
+		giveHeroBonus(&gb);
 	}
 	else if (cheat == "vcmiformenos")
 	{

+ 1 - 1
server/CGameHandler.h

@@ -196,7 +196,7 @@ public:
 	bool makeBattleAction(BattleAction &ba);
 	bool makeAutomaticAction(const CStack *stack, BattleAction &ba); //used when action is taken by stack without volition of player (eg. unguided catapult attack)
 	bool makeCustomAction(BattleAction &ba);
-	void stackAppearTrigger(const CStack *stack);
+	void stackEnchantedTrigger(const CStack * stack);
 	void stackTurnTrigger(const CStack *stack);
 	void handleDamageFromObstacle(const CObstacleInstance &obstacle, const CStack * curStack); //checks if obstacle is land mine and handles possible consequences
 	void removeObstacle(const CObstacleInstance &obstacle);

+ 6 - 2
test/CMapEditManagerTest.cpp

@@ -21,7 +21,9 @@
 #include "../lib/CRandomGenerator.h"
 #include "../lib/VCMI_Lib.h"
 
-BOOST_AUTO_TEST_CASE(CMapEditManager_DrawTerrain_Type)
+BOOST_AUTO_TEST_SUITE(CMapEditManager_Suite)
+
+BOOST_AUTO_TEST_CASE(DrawTerrain_Type)
 {
 	logGlobal->info("CMapEditManager_DrawTerrain_Type start");
 	try
@@ -110,7 +112,7 @@ BOOST_AUTO_TEST_CASE(CMapEditManager_DrawTerrain_Type)
 	logGlobal->info("CMapEditManager_DrawTerrain_Type finish");
 }
 
-BOOST_AUTO_TEST_CASE(CMapEditManager_DrawTerrain_View)
+BOOST_AUTO_TEST_CASE(DrawTerrain_View)
 {
 	logGlobal->info("CMapEditManager_DrawTerrain_View start");
 	try
@@ -178,3 +180,5 @@ BOOST_AUTO_TEST_CASE(CMapEditManager_DrawTerrain_View)
 	}
 	logGlobal->info("CMapEditManager_DrawTerrain_View finish");
 }
+
+BOOST_AUTO_TEST_SUITE_END()

+ 6 - 2
test/CMapFormatTest.cpp

@@ -26,7 +26,9 @@
 
 static const int TEST_RANDOM_SEED = 1337;
 
-BOOST_AUTO_TEST_CASE(MapFormat_Random)
+BOOST_AUTO_TEST_SUITE(MapFormat_Suite)
+
+BOOST_AUTO_TEST_CASE(Random)
 {
 	logGlobal->info("MapFormat_Random start");
 	BOOST_TEST_CHECKPOINT("MapFormat_Random start");
@@ -115,7 +117,7 @@ static void addToArchive(CZipSaver & saver, const JsonNode & data, const std::st
 	}
 }
 
-BOOST_AUTO_TEST_CASE(MapFormat_Objects)
+BOOST_AUTO_TEST_CASE(Objects)
 {
 	logGlobal->info("MapFormat_Objects start");
 
@@ -202,3 +204,5 @@ BOOST_AUTO_TEST_CASE(MapFormat_Objects)
 
 	logGlobal->info("MapFormat_Objects finish");
 }
+
+BOOST_AUTO_TEST_SUITE_END()

+ 15 - 14
test/CMemoryBufferTest.cpp

@@ -9,44 +9,45 @@
  *
  */
 #include "StdInc.h"
-
 #include <boost/test/unit_test.hpp>
-
 #include "../lib/filesystem/CMemoryBuffer.h"
 
-
 struct CMemoryBufferFixture
 {
 	CMemoryBuffer subject;
 };
 
-BOOST_FIXTURE_TEST_CASE(CMemoryBuffer_Empty, CMemoryBufferFixture)
+BOOST_AUTO_TEST_SUITE(CMemoryBuffer_Suite)
+
+BOOST_FIXTURE_TEST_CASE(empty, CMemoryBufferFixture)
 {
 	BOOST_REQUIRE_EQUAL(0, subject.tell());
 	BOOST_REQUIRE_EQUAL(0, subject.getSize());
-	
+
 	si32 dummy = 1337;
-	
+
 	auto ret = subject.read((ui8 *)&dummy, sizeof(si32));
-	
-	BOOST_CHECK_EQUAL(0, ret);	
+
+	BOOST_CHECK_EQUAL(0, ret);
 	BOOST_CHECK_EQUAL(1337, dummy);
 	BOOST_CHECK_EQUAL(0, subject.tell());
 }
 
-BOOST_FIXTURE_TEST_CASE(CMemoryBuffer_Write, CMemoryBufferFixture)
+BOOST_FIXTURE_TEST_CASE(write, CMemoryBufferFixture)
 {
 	const si32 initial = 1337;
-	
+
 	subject.write((const ui8 *)&initial, sizeof(si32));
-	
+
 	BOOST_CHECK_EQUAL(4, subject.tell());
 	subject.seek(0);
 	BOOST_CHECK_EQUAL(0, subject.tell());
-	
+
 	si32 current = 0;
 	auto ret = subject.read((ui8 *)&current, sizeof(si32));
-	BOOST_CHECK_EQUAL(sizeof(si32), ret);	
+	BOOST_CHECK_EQUAL(sizeof(si32), ret);
 	BOOST_CHECK_EQUAL(initial, current);
-	BOOST_CHECK_EQUAL(4, subject.tell());	
+	BOOST_CHECK_EQUAL(4, subject.tell());
 }
+
+BOOST_AUTO_TEST_SUITE_END()