Browse Source

Merge beta -> develop

Ivan Savenko 2 năm trước cách đây
mục cha
commit
fb739e7186

+ 113 - 1
.github/workflows/github.yml

@@ -130,7 +130,7 @@ jobs:
             preset: android-conan-ninja-release
             conan_profile: android-64
             conan_options: --conf tools.android:ndk_path=$ANDROID_NDK_ROOT
-            artifact_platform: aarch64-v8a
+            artifact_platform: arm64-v8a
     runs-on: ${{ matrix.os }}
     defaults:
       run:
@@ -225,6 +225,7 @@ jobs:
         name: ${{ env.VCMI_PACKAGE_FILE_NAME }} - ${{ matrix.platform }}
         path: |
           ${{github.workspace}}/out/build/${{matrix.preset}}/${{ env.VCMI_PACKAGE_FILE_NAME }}.${{ matrix.extension }}
+          
     - name: Android artifacts
       if: ${{ startsWith(matrix.platform, 'android') }}
       uses: actions/upload-artifact@v3
@@ -233,6 +234,14 @@ jobs:
         path: |
           ${{ env.ANDROID_APK_PATH }}
 
+    - name: Android JNI ${{matrix.platform}}
+      if: ${{ startsWith(matrix.platform, 'android') && github.ref == 'refs/heads/master' }}
+      uses: actions/upload-artifact@v3
+      with:
+        name: Android JNI ${{matrix.platform}}
+        path: |
+          ${{ github.workspace }}/android/vcmi-app/src/main/jniLibs
+
     - name: Upload build
       if: ${{ (matrix.pack == 1 || startsWith(matrix.platform, 'android')) && (github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/beta' || github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/heads/features/')) && matrix.platform != 'msvc' }}
       continue-on-error: true
@@ -254,3 +263,106 @@ jobs:
       env:
         SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
       if: always()
+  
+  # copy-pasted mostly
+  bundle_release:
+    
+    needs: build
+    if: always() && github.ref == 'refs/heads/master'
+    strategy:
+      matrix:
+        include:
+          - platform: android-32
+            os: ubuntu-22.04
+            extension: aab
+            preset: android-conan-ninja-release
+            conan_profile: android-32
+            conan_options: --conf tools.android:ndk_path=$ANDROID_NDK_ROOT
+            artifact_platform: aab
+    runs-on: ${{ matrix.os }}
+    defaults:
+      run:
+        shell: bash
+
+    steps:
+    - uses: actions/checkout@v3
+      with:
+        submodules: recursive
+
+    - name: Dependencies
+      run: source '${{github.workspace}}/CI/${{matrix.platform}}/before_install.sh'
+      env:
+        VCMI_BUILD_PLATFORM: x64
+
+    - uses: actions/setup-python@v4
+      if: "${{ matrix.conan_profile != '' }}"
+      with:
+        python-version: '3.10'
+    - name: Conan setup
+      if: "${{ matrix.conan_profile != '' }}"
+      run: |
+        pip3 install 'conan<2.0'
+        conan profile new default --detect
+        conan install . \
+          --install-folder=conan-generated \
+          --no-imports \
+          --build=never \
+          --profile:build=default \
+          --profile:host=CI/conan/${{ matrix.conan_profile }} \
+          ${{ matrix.conan_options }}
+      env:
+        GENERATE_ONLY_BUILT_CONFIG: 1
+
+    - name: Git branch name
+      id: git-branch-name
+      uses: EthanSK/git-branch-name-action@v1
+
+    - name: Build Number
+      run: |
+        source '${{github.workspace}}/CI/get_package_name.sh'
+        if [ '${{ matrix.artifact_platform }}' ]; then
+          VCMI_PACKAGE_FILE_NAME+="-${{ matrix.artifact_platform }}"
+        fi
+        echo VCMI_PACKAGE_FILE_NAME="$VCMI_PACKAGE_FILE_NAME" >> $GITHUB_ENV
+        echo VCMI_PACKAGE_NAME_SUFFIX="$VCMI_PACKAGE_NAME_SUFFIX" >> $GITHUB_ENV
+        echo VCMI_PACKAGE_GITVERSION="$VCMI_PACKAGE_GITVERSION" >> $GITHUB_ENV
+      env:
+        PULL_REQUEST: ${{ github.event.pull_request.number }}
+
+    - name: CMake Preset
+      run: |
+        cmake --preset ${{ matrix.preset }}
+
+    - name: Build Preset
+      run: |
+        cmake --build --preset ${{matrix.preset}}
+
+    - name: Download libs x64
+      uses: actions/download-artifact@v3
+      with:
+        name: Android JNI android-64
+        path: ${{ github.workspace }}/android/vcmi-app/src/main/jniLibs/
+ 
+    - name: Create android package
+      run: |
+        cd android
+        ./gradlew bundleRelease --info
+        echo ANDROID_APK_PATH="$(ls ${{ github.workspace }}/android/vcmi-app/build/outputs/bundle/release/*.aab)" >> $GITHUB_ENV
+      env:
+        ANDROID_STORE_PASSWORD: ${{ secrets.ANDROID_STORE_PASSWORD }}
+        ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
+
+    - name: Android artifacts
+      uses: actions/upload-artifact@v3
+      with:
+        name: ${{ env.VCMI_PACKAGE_FILE_NAME }}
+        path: |
+          ${{ env.ANDROID_APK_PATH }}
+
+    - uses: act10ns/slack@v1
+      with:
+        status: ${{ job.status }}
+        channel: '#notifications'
+      env:
+        SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
+      if: always()

BIN
CI/android/android-release.jks


+ 0 - 0
CI/android/signing.properties → CI/android/dailySigning.properties


+ 2 - 0
CI/android/releaseSigning.properties

@@ -0,0 +1,2 @@
+STORE_FILE=android-release.jks
+KEY_ALIAS=vcmi

+ 1 - 0
CMakeLists.txt

@@ -251,6 +251,7 @@ if(MINGW OR MSVC)
 		add_definitions(-D_CRT_SECURE_NO_WARNINGS)
 		add_definitions(-D_SCL_SECURE_NO_WARNINGS)
 
+		set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /utf-8")
 		set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /bigobj")
 		set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4250") # 4250: 'class1' : inherits 'class2::member' via dominance
 		set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4251") # 4251: class 'xxx' needs to have dll-interface to be used by clients of class 'yyy'

+ 21 - 1
ChangeLog.md

@@ -1,6 +1,26 @@
-# 1.2.0 -> 1.3.0
+# 1.2.1 -> 1.3.0
 (unreleased)
 
+# 1.2.0 -> 1.2.1
+
+### GENERAL:
+* Implemented spell range overlay for Dimension Door and Scuttle Boat
+* Fixed movement cost penalty from terrain
+* Fixed empty Black Market on game start
+* Fixed bad morale happening after waiting
+* Fixed good morale happening after defeating last enemy unit
+* Fixed death animation of Efreeti killed by petrification attack
+* Fixed crash on leaving to main menu from battle in hotseat mode
+* Adventure map spells are no longer visible on units in battle
+* Attempt to cast spell with no valid targets in hotseat will show appropriate error message
+* RMG settings will now show all existing in game templates and not just those suitable for current settings
+* RMG settings (map size and two-level maps) that are not compatible with current template will be blocked
+* Fixed centering of scenario information window
+* Fixed crash on empty save game list after filtering
+* Fixed blocked progress in Launcher on language detection failure
+* Launcher will now correctly handle selection of Ddata directory in H3 install
+* Map editor will now correctly save message property for events and pandoras
+
 # 1.1.1 -> 1.2.0
 
 ### GENERAL:

BIN
Mods/vcmi/Data/debug/cached.png


BIN
Mods/vcmi/Data/debug/spellRange.png


+ 30 - 17
android/vcmi-app/build.gradle

@@ -10,14 +10,16 @@ android {
 		applicationId "is.xyz.vcmi"
 		minSdk 19
 		targetSdk 31
-		versionCode 1103
-		versionName "1.1"
+		versionCode 1200
+		versionName "1.2"
 		setProperty("archivesBaseName", "vcmi")
 	}
 
 	signingConfigs {
 		releaseSigning
-		LoadSigningConfig()
+		dailySigning
+		LoadSigningConfig("releaseSigning")
+		LoadSigningConfig("dailySigning")
 	}
 
 	buildTypes {
@@ -46,6 +48,7 @@ android {
 		daily {
 			initWith release
 			applicationIdSuffix '.daily'
+			signingConfig signingConfigs.dailySigning
 			manifestPlaceholders = [
 				applicationLabel: 'VCMI daily',
 			]
@@ -118,38 +121,48 @@ def ResolveGitInfo() {
 		CommandOutput("git", ["describe", "--match=", "--always", "--abbrev=7"], ".")
 }
 
-def SigningPropertiesPath(final basePath) {
-	return file("${basePath}/signing.properties")
+def SigningPropertiesPath(final basePath, final signingConfigKey) {
+	return file("${basePath}/${signingConfigKey}.properties")
 }
 
 def SigningKeystorePath(final basePath, final keystoreFileName) {
 	return file("${basePath}/${keystoreFileName}")
 }
 
-def LoadSigningConfig() {
+def LoadSigningConfig(final signingConfigKey) {
 	final def projectRoot = "${project.projectDir}/../../CI/android"
 	final def props = new Properties()
-	final def propFile = SigningPropertiesPath(projectRoot)
+	final def propFile = SigningPropertiesPath(projectRoot, signingConfigKey)
+	
+	def signingConfig = android.signingConfigs.getAt(signingConfigKey)
+	
 	if (propFile.canRead()) {
 		props.load(new FileInputStream(propFile))
 
 		if (props != null
 			&& props.containsKey('STORE_FILE')
-			&& props.containsKey('STORE_PASSWORD')
-			&& props.containsKey('KEY_ALIAS')
-			&& props.containsKey('KEY_PASSWORD')) {
-
-			android.signingConfigs.releaseSigning.storeFile = SigningKeystorePath(projectRoot, props['STORE_FILE'])
-			android.signingConfigs.releaseSigning.storePassword = props['STORE_PASSWORD']
-			android.signingConfigs.releaseSigning.keyAlias = props['KEY_ALIAS']
-			android.signingConfigs.releaseSigning.keyPassword = props['KEY_PASSWORD']
+			&& props.containsKey('KEY_ALIAS')) {
+
+			signingConfig.storeFile = SigningKeystorePath(projectRoot, props['STORE_FILE'])
+			signingConfig.storePassword = props['STORE_PASSWORD']
+			signingConfig.keyAlias = props['KEY_ALIAS']
+			
+			if(props.containsKey('STORE_PASSWORD'))
+				signingConfig.storePassword = props['STORE_PASSWORD']
+			else
+				signingConfig.storePassword = System.getenv("ANDROID_STORE_PASSWORD")
+			
+			if(props.containsKey('KEY_PASSWORD'))
+				signingConfig.keyPassword = props['KEY_PASSWORD']
+			else
+				signingConfig.keyPassword = System.getenv("ANDROID_KEY_PASSWORD")
 		} else {
 			println("Some props from signing file are missing")
-			android.buildTypes.release.signingConfig = null
+			android.signingConfigs.putAt(signingConfigKey, null)
 		}
 	} else {
 		println("file with signing properties is missing")
-		android.buildTypes.release.signingConfig = null
+		android.signingConfigs.putAt(signingConfigKey, null)
 	}
 }
 

+ 1 - 0
client/Client.cpp

@@ -374,6 +374,7 @@ void CClient::endGame()
 	//threads cleanup has to be after gs cleanup and before battleints cleanup to stop tacticThread
 	cleanThreads();
 
+	CPlayerInterface::battleInt.reset();
 	playerint.clear();
 	battleints.clear();
 	battleCallbacks.clear();

+ 14 - 2
client/adventureMap/CAdventureMapInterface.cpp

@@ -992,8 +992,11 @@ void CAdventureMapInterface::onTileLeftClicked(const int3 &mapPos)
 	const CGObjectInstance *topBlocking = getActiveObject(mapPos);
 
 	int3 selPos = LOCPLINT->localState->getCurrentArmy()->getSightCenter();
-	if(spellBeingCasted && isInScreenRange(selPos, mapPos))
+	if(spellBeingCasted)
 	{
+		if (!isInScreenRange(selPos, mapPos))
+			return;
+
 		const TerrainTile *heroTile = LOCPLINT->cb->getTile(selPos);
 
 		switch(spellBeingCasted->id)
@@ -1099,11 +1102,15 @@ void CAdventureMapInterface::onTileHovered(const int3 &mapPos)
 		switch(spellBeingCasted->id)
 		{
 		case SpellID::SCUTTLE_BOAT:
-			if(objAtTile && objAtTile->ID == Obj::BOAT)
+			{
+			int3 hpos = LOCPLINT->localState->getCurrentArmy()->getSightCenter();
+
+			if(objAtTile && objAtTile->ID == Obj::BOAT && isInScreenRange(hpos, mapPos))
 				CCS->curh->set(Cursor::Map::SCUTTLE_BOAT);
 			else
 				CCS->curh->set(Cursor::Map::POINTER);
 			return;
+			}
 		case SpellID::DIMENSION_DOOR:
 			{
 				const TerrainTile * t = LOCPLINT->cb->getTile(mapPos, false);
@@ -1264,6 +1271,8 @@ void CAdventureMapInterface::enterCastingMode(const CSpell * sp)
 {
 	assert(sp->id == SpellID::SCUTTLE_BOAT || sp->id == SpellID::DIMENSION_DOOR);
 	spellBeingCasted = sp;
+	Settings config = settings.write["session"]["showSpellRange"];
+	config->Bool() = true;
 
 	deactivate();
 	terrain->activate();
@@ -1276,6 +1285,9 @@ void CAdventureMapInterface::exitCastingMode()
 	spellBeingCasted = nullptr;
 	terrain->deactivate();
 	activate();
+
+	Settings config = settings.write["session"]["showSpellRange"];
+	config->Bool() = false;
 }
 
 void CAdventureMapInterface::abortCastingMode()

+ 1 - 1
client/battle/BattleAnimationClasses.cpp

@@ -137,7 +137,7 @@ bool StackActionAnimation::init()
 
 StackActionAnimation::~StackActionAnimation()
 {
-	if (stack->isFrozen())
+	if (stack->isFrozen() && currGroup != ECreatureAnimType::DEATH && currGroup != ECreatureAnimType::DEATH_RANGED)
 		myAnim->setType(ECreatureAnimType::HOLDING);
 	else
 		myAnim->setType(nextGroup);

+ 1 - 1
client/lobby/CLobbyScreen.cpp

@@ -206,7 +206,7 @@ void CLobbyScreen::updateAfterStateChange()
 		}
 	}
 	
-	if(curTab == tabRand && CSH->si->mapGenOptions)
+	if(curTab && curTab == tabRand && CSH->si->mapGenOptions)
 		tabRand->setMapGenOptions(CSH->si->mapGenOptions);
 }
 

+ 3 - 4
client/lobby/CSavingScreen.cpp

@@ -31,8 +31,6 @@ CSavingScreen::CSavingScreen()
 {
 	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
 	center(pos);
-	// TODO: we should really use std::shared_ptr for passing StartInfo around.
-	localSi = new StartInfo(*LOCPLINT->cb->getStartInfo());
 	localMi = std::make_shared<CMapInfo>();
 	localMi->mapHeader = std::unique_ptr<CMapHeader>(new CMapHeader(*LOCPLINT->cb->getMapHeader()));
 
@@ -52,7 +50,9 @@ const CMapInfo * CSavingScreen::getMapInfo()
 
 const StartInfo * CSavingScreen::getStartInfo()
 {
-	return localSi;
+	if (localMi)
+		return localMi->scenarioOptionsOfSave;
+	return LOCPLINT->cb->getStartInfo();
 }
 
 void CSavingScreen::changeSelection(std::shared_ptr<CMapInfo> to)
@@ -61,7 +61,6 @@ void CSavingScreen::changeSelection(std::shared_ptr<CMapInfo> to)
 		return;
 
 	localMi = to;
-	localSi = localMi->scenarioOptionsOfSave;
 	card->changeSelection();
 }
 

+ 0 - 1
client/lobby/CSavingScreen.h

@@ -23,7 +23,6 @@ class CSelectionBase;
 class CSavingScreen : public CSelectionBase
 {
 public:
-	const StartInfo * localSi;
 	std::shared_ptr<CMapInfo> localMi;
 
 	CSavingScreen();

+ 4 - 0
client/lobby/CScenarioInfoScreen.cpp

@@ -26,6 +26,10 @@
 CScenarioInfoScreen::CScenarioInfoScreen()
 {
 	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
+	pos.w = 800;
+	pos.h = 600;
+	pos = center();
+
 	localSi = new StartInfo(*LOCPLINT->cb->getStartInfo());
 	localMi = new CMapInfo();
 	localMi->mapHeader = std::unique_ptr<CMapHeader>(new CMapHeader(*LOCPLINT->cb->getMapHeader()));

+ 25 - 1
client/lobby/RandomMapTab.cpp

@@ -242,9 +242,29 @@ void RandomMapTab::setMapGenOptions(std::shared_ptr<CMapGenOptions> opts)
 	}
 	
 	if(auto w = widget<CToggleGroup>("groupMapSize"))
+	{
+		for(auto toggle : w->buttons)
+		{
+			if(auto button = std::dynamic_pointer_cast<CToggleButton>(toggle.second))
+			{
+				const auto & mapSizes = getPossibleMapSizes();
+				int3 size( mapSizes[toggle.first], mapSizes[toggle.first], 1 + mapGenOptions->getHasTwoLevels());
+
+				bool sizeAllowed = !mapGenOptions->getMapTemplate() || mapGenOptions->getMapTemplate()->matchesSize(size);
+				button->block(!sizeAllowed);
+			}
+		}
 		w->setSelected(vstd::find_pos(getPossibleMapSizes(), opts->getWidth()));
+	}
 	if(auto w = widget<CToggleButton>("buttonTwoLevels"))
+	{
+		int3 size( opts->getWidth(), opts->getWidth(), 2);
+
+		bool undergoundAllowed = !mapGenOptions->getMapTemplate() || mapGenOptions->getMapTemplate()->matchesSize(size);
+
 		w->setSelected(opts->getHasTwoLevels());
+		w->block(!undergoundAllowed);
+	}
 	if(auto w = widget<CToggleGroup>("groupMaxPlayers"))
 	{
 		w->setSelected(opts->getPlayerCount());
@@ -408,7 +428,11 @@ TemplatesDropBox::TemplatesDropBox(RandomMapTab & randomMapTab, int3 size):
 	REGISTER_BUILDER("templateListItem", &TemplatesDropBox::buildListItem);
 	
 	curItems = VLC->tplh->getTemplates();
-	vstd::erase_if(curItems, [size](const CRmgTemplate * t){return !t->matchesSize(size);});
+
+	boost::range::sort(curItems, [](const CRmgTemplate * a, const CRmgTemplate * b){
+		return a->getName() < b->getName();
+	});
+
 	curItems.insert(curItems.begin(), nullptr); //default template
 	
 	const JsonNode config(ResourceID("config/widgets/randomMapTemplateWidget.json"));

+ 4 - 0
client/mapView/IMapRendererContext.h

@@ -86,4 +86,8 @@ public:
 	virtual bool showGrid() const = 0;
 	virtual bool showVisitable() const = 0;
 	virtual bool showBlocked() const = 0;
+
+	/// if true, spell range for teleport / scuttle boat will be visible
+	virtual bool showSpellRange(const int3 & position) const = 0;
+
 };

+ 12 - 5
client/mapView/MapRenderer.cpp

@@ -565,15 +565,16 @@ uint8_t MapRendererObjects::checksum(IMapRendererContext & context, const int3 &
 	return 0xff-1;
 }
 
-MapRendererDebug::MapRendererDebug()
+MapRendererOverlay::MapRendererOverlay()
 	: imageGrid(IImage::createFromFile("debug/grid", EImageBlitMode::ALPHA))
 	, imageBlocked(IImage::createFromFile("debug/blocked", EImageBlitMode::ALPHA))
 	, imageVisitable(IImage::createFromFile("debug/visitable", EImageBlitMode::ALPHA))
+	, imageSpellRange(IImage::createFromFile("debug/spellRange", EImageBlitMode::ALPHA))
 {
 
 }
 
-void MapRendererDebug::renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates)
+void MapRendererOverlay::renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates)
 {
 	if(context.showGrid())
 		target.draw(imageGrid, Point(0,0));
@@ -599,9 +600,12 @@ void MapRendererDebug::renderTile(IMapRendererContext & context, Canvas & target
 		if (context.showVisitable() && visitable)
 			target.draw(imageVisitable, Point(0,0));
 	}
+
+	if (context.showSpellRange(coordinates))
+		target.draw(imageSpellRange, Point(0,0));
 }
 
-uint8_t MapRendererDebug::checksum(IMapRendererContext & context, const int3 & coordinates)
+uint8_t MapRendererOverlay::checksum(IMapRendererContext & context, const int3 & coordinates)
 {
 	uint8_t result = 0;
 
@@ -614,6 +618,9 @@ uint8_t MapRendererDebug::checksum(IMapRendererContext & context, const int3 & c
 	if (context.showGrid())
 		result += 4;
 
+	if (context.showSpellRange(coordinates))
+		result += 8;
+
 	return result;
 }
 
@@ -747,7 +754,7 @@ MapRenderer::TileChecksum MapRenderer::getTileChecksum(IMapRendererContext & con
 			result[3] = rendererRoad.checksum(context, coordinates);
 		result[4] = rendererObjects.checksum(context, coordinates);
 		result[5] = rendererPath.checksum(context, coordinates);
-		result[6] = rendererDebug.checksum(context, coordinates);
+		result[6] = rendererOverlay.checksum(context, coordinates);
 
 		if(!context.isVisible(coordinates))
 			result[7] = rendererFow.checksum(context, coordinates);
@@ -781,7 +788,7 @@ void MapRenderer::renderTile(IMapRendererContext & context, Canvas & target, con
 
 		rendererObjects.renderTile(context, target, coordinates);
 		rendererPath.renderTile(context, target, coordinates);
-		rendererDebug.renderTile(context, target, coordinates);
+		rendererOverlay.renderTile(context, target, coordinates);
 
 		if(!context.isVisible(coordinates))
 			rendererFow.renderTile(context, target, coordinates);

+ 3 - 12
client/mapView/MapRenderer.h

@@ -129,21 +129,12 @@ public:
 	void renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates);
 };
 
-class MapRendererDebug
+class MapRendererOverlay
 {
 	std::shared_ptr<IImage> imageGrid;
 	std::shared_ptr<IImage> imageVisitable;
 	std::shared_ptr<IImage> imageBlocked;
-public:
-	MapRendererDebug();
-
-	uint8_t checksum(IMapRendererContext & context, const int3 & coordinates);
-	void renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates);
-};
-
-class MapRendererOverlay
-{
-	std::unique_ptr<CAnimation> iconsStorage;
+	std::shared_ptr<IImage> imageSpellRange;
 public:
 	MapRendererOverlay();
 
@@ -160,7 +151,7 @@ class MapRenderer
 	MapRendererFow rendererFow;
 	MapRendererObjects rendererObjects;
 	MapRendererPath rendererPath;
-	MapRendererDebug rendererDebug;
+	MapRendererOverlay rendererOverlay;
 
 public:
 	using TileChecksum = std::array<uint8_t, 8>;

+ 19 - 0
client/mapView/MapRendererContext.cpp

@@ -22,6 +22,7 @@
 #include "../../lib/CPathfinder.h"
 #include "../../lib/Point.h"
 #include "../../lib/mapObjects/CGHeroInstance.h"
+#include "../../lib/spells/CSpellHandler.h"
 #include "../../lib/mapping/CMap.h"
 
 MapRendererBaseContext::MapRendererBaseContext(const MapRendererContextState & viewState)
@@ -199,6 +200,11 @@ bool MapRendererBaseContext::showBlocked() const
 	return false;
 }
 
+bool MapRendererBaseContext::showSpellRange(const int3 & position) const
+{
+	return false;
+}
+
 MapRendererAdventureContext::MapRendererAdventureContext(const MapRendererContextState & viewState)
 	: MapRendererBaseContext(viewState)
 {
@@ -266,6 +272,19 @@ bool MapRendererAdventureContext::showBlocked() const
 	return settingShowBlocked;
 }
 
+bool MapRendererAdventureContext::showSpellRange(const int3 & position) const
+{
+	if (!settingSpellRange)
+		return false;
+
+	auto hero = LOCPLINT->localState->getCurrentHero();
+
+	if (!hero)
+		return false;
+
+	return !isInScreenRange(hero->getSightCenter(), position);
+}
+
 MapRendererAdventureTransitionContext::MapRendererAdventureTransitionContext(const MapRendererContextState & viewState)
 	: MapRendererAdventureContext(viewState)
 {

+ 4 - 0
client/mapView/MapRendererContext.h

@@ -58,6 +58,7 @@ public:
 	bool showGrid() const override;
 	bool showVisitable() const override;
 	bool showBlocked() const override;
+	bool showSpellRange(const int3 & position) const override;
 };
 
 class MapRendererAdventureContext : public MapRendererBaseContext
@@ -67,6 +68,7 @@ public:
 	bool settingShowGrid = false;
 	bool settingShowVisitable = false;
 	bool settingShowBlocked = false;
+	bool settingSpellRange= false;
 	bool settingsAdventureObjectAnimation = true;
 	bool settingsAdventureTerrainAnimation = true;
 
@@ -80,6 +82,8 @@ public:
 	bool showGrid() const override;
 	bool showVisitable() const override;
 	bool showBlocked() const override;
+
+	bool showSpellRange(const int3 & position) const override;
 };
 
 class MapRendererAdventureTransitionContext : public MapRendererAdventureContext

+ 2 - 1
client/mapView/MapView.cpp

@@ -160,6 +160,7 @@ void MapView::onViewMapActivated()
 PuzzleMapView::PuzzleMapView(const Point & offset, const Point & dimensions, const int3 & tileToCenter)
 	: BasicMapView(offset, dimensions)
 {
-	controller->setViewCenter(tileToCenter);
 	controller->activatePuzzleMapContext(tileToCenter);
+	controller->setViewCenter(tileToCenter);
+
 }

+ 2 - 1
client/mapView/MapViewController.cpp

@@ -63,7 +63,7 @@ void MapViewController::setViewCenter(const Point & position, int level)
 	model->setViewCenter(betterPosition);
 	model->setLevel(std::clamp(level, 0, context->getMapSize().z));
 
-	if(adventureInt) // may be called before adventureInt is initialized
+	if(adventureInt && !puzzleMapContext) // may be called before adventureInt is initialized
 		adventureInt->onMapViewMoved(model->getTilesTotalRect(), model->getLevel());
 }
 
@@ -154,6 +154,7 @@ void MapViewController::updateBefore(uint32_t timeDelta)
 		adventureContext->settingShowGrid = settings["gameTweaks"]["showGrid"].Bool();
 		adventureContext->settingShowVisitable = settings["session"]["showVisitable"].Bool();
 		adventureContext->settingShowBlocked = settings["session"]["showBlocked"].Bool();
+		adventureContext->settingSpellRange = settings["session"]["showSpellRange"].Bool();
 	}
 }
 

+ 4 - 4
client/windows/CSpellWindow.cpp

@@ -510,7 +510,7 @@ void CSpellWindow::SpellArea::clickLeft(tribool down, bool previousState)
 		auto spellCost = owner->myInt->cb->getSpellCost(mySpell, owner->myHero);
 		if(spellCost > owner->myHero->mana) //insufficient mana
 		{
-			owner->myInt->showInfoDialog(boost::str(boost::format(CGI->generaltexth->allTexts[206]) % spellCost % owner->myHero->mana));
+			LOCPLINT->showInfoDialog(boost::str(boost::format(CGI->generaltexth->allTexts[206]) % spellCost % owner->myHero->mana));
 			return;
 		}
 
@@ -530,7 +530,7 @@ void CSpellWindow::SpellArea::clickLeft(tribool down, bool previousState)
 		if((combatSpell ^ inCombat) || inCastle)
 		{
 			std::vector<std::shared_ptr<CComponent>> hlp(1, std::make_shared<CComponent>(CComponent::spell, mySpell->id, 0));
-			owner->myInt->showInfoDialog(mySpell->getDescriptionTranslated(schoolLevel), hlp);
+			LOCPLINT->showInfoDialog(mySpell->getDescriptionTranslated(schoolLevel), hlp);
 		}
 		else if(combatSpell)
 		{
@@ -545,9 +545,9 @@ void CSpellWindow::SpellArea::clickLeft(tribool down, bool previousState)
 				std::vector<std::string> texts;
 				problem.getAll(texts);
 				if(!texts.empty())
-					owner->myInt->showInfoDialog(texts.front());
+					LOCPLINT->showInfoDialog(texts.front());
 				else
-					owner->myInt->showInfoDialog(CGI->generaltexth->translate("vcmi.adventureMap.spellUnknownProblem"));
+					LOCPLINT->showInfoDialog(CGI->generaltexth->translate("vcmi.adventureMap.spellUnknownProblem"));
 			}
 		}
 		else //adventure spell

+ 8 - 2
debian/changelog

@@ -3,13 +3,19 @@ vcmi (1.3.0) jammy; urgency=medium
   * New upstream release
 
  -- Ivan Savenko <[email protected]>  Sat, 01 Jul 2023 16:00:00 +0200
- 
+
+vcmi (1.2.1) jammy; urgency=medium
+
+  * New upstream release
+
+ -- Ivan Savenko <[email protected]>  Fri, 28 Apr 2023 16:00:00 +0200
+
 vcmi (1.2.0) jammy; urgency=medium
 
   * New upstream release
 
  -- Ivan Savenko <[email protected]>  Fri, 14 Apr 2023 16:00:00 +0200
- 
+
 vcmi (1.1.1) jammy; urgency=medium
 
   * New upstream release

+ 1 - 0
launcher/eu.vcmi.VCMI.metainfo.xml

@@ -52,6 +52,7 @@
 	</categories>
 	<releases>
 		<release version="1.3.0" date="2023-07-01" type="development" />
+		<release version="1.2.1" date="2023-04-28" />
 		<release version="1.2.0" date="2023-04-14" />
 		<release version="1.1.1" date="2023-02-03" />
 		<release version="1.1.0" date="2022-12-23" />

+ 14 - 1
launcher/firstLaunch/firstlaunch_moc.cpp

@@ -204,6 +204,7 @@ void FirstLaunchView::heroesDataMissing()
 	ui->labelDataCopy->setVisible(true);
 
 	ui->labelDataFound->setVisible(false);
+	ui->pushButtonDataNext->setEnabled(false);
 
 	if(hasVCMIBuilderScript)
 	{
@@ -232,6 +233,7 @@ void FirstLaunchView::heroesDataDetected()
 	}
 
 	ui->labelDataFound->setVisible(true);
+	ui->pushButtonDataNext->setEnabled(true);
 
 	heroesLanguageUpdate();
 }
@@ -261,7 +263,6 @@ void FirstLaunchView::heroesLanguageUpdate()
 
 	ui->labelDataFailure->setVisible(!success);
 	ui->labelDataSuccess->setVisible(success);
-	ui->pushButtonDataNext->setEnabled(success);
 }
 
 void FirstLaunchView::forceHeroesLanguage(const QString & language)
@@ -278,6 +279,18 @@ void FirstLaunchView::copyHeroesData()
 	if(!sourceRoot.exists())
 		return;
 
+	if (sourceRoot.dirName().compare("data", Qt::CaseInsensitive) == 0)
+	{
+		// We got Data folder. Possibly user selected "Data" folder of Heroes III install. Check whether valid data might exist 1 level above
+
+		QStringList dirData = sourceRoot.entryList({"data"}, QDir::Filter::Dirs);
+		if (dirData.empty())
+		{
+			// This is "Data" folder without any "Data" folders inside. Try to check for data 1 level above
+			sourceRoot.cdUp();
+		}
+	}
+
 	QStringList dirData = sourceRoot.entryList({"data"}, QDir::Filter::Dirs);
 	QStringList dirMaps = sourceRoot.entryList({"maps"}, QDir::Filter::Dirs);
 	QStringList dirMp3 = sourceRoot.entryList({"mp3"}, QDir::Filter::Dirs);

+ 5 - 5
launcher/jsonutils.cpp

@@ -96,14 +96,14 @@ JsonNode toJson(QVariant object)
 {
 	JsonNode ret;
 
-	if(object.canConvert<QVariantMap>())
-		ret.Struct() = VariantToMap(object.toMap());
-	else if(object.canConvert<QVariantList>())
-		ret.Vector() = VariantToList(object.toList());
-	else if(object.userType() == QMetaType::QString)
+	if(object.userType() == QMetaType::QString)
 		ret.String() = object.toString().toUtf8().data();
 	else if(object.userType() == QMetaType::Bool)
 		ret.Bool() = object.toBool();
+	else if(object.canConvert<QVariantMap>())
+		ret.Struct() = VariantToMap(object.toMap());
+	else if(object.canConvert<QVariantList>())
+		ret.Vector() = VariantToList(object.toList());
 	else if(object.canConvert<int>())
 		ret.Integer() = object.toInt();
 	else if(object.canConvert<double>())

+ 1 - 1
launcher/languages.cpp

@@ -44,7 +44,7 @@ QString Languages::getHeroesDataLanguage()
 	QString language = QString::fromStdString(settings["session"]["language"].String());
 	double deviation = settings["session"]["languageDeviation"].Float();
 
-	if(deviation > 0.05)
+	if(deviation > 0.1)
 		return QString();
 	return language;
 }

+ 1 - 1
lib/CStack.cpp

@@ -128,7 +128,7 @@ std::vector<si32> CStack::activeSpells() const
 	CSelector selector = Selector::sourceType()(Bonus::SPELL_EFFECT)
 						 .And(CSelector([](const Bonus * b)->bool
 	{
-		return b->type != Bonus::NONE;
+		return b->type != Bonus::NONE && SpellID(b->sid).toSpell() && !SpellID(b->sid).toSpell()->isAdventure();
 	}));
 
 	TConstBonusListPtr spellEffects = getBonuses(selector, Selector::all, cachingStr.str());

+ 1 - 1
lib/mapObjects/CGHeroInstance.cpp

@@ -76,7 +76,7 @@ ui32 CGHeroInstance::getTileCost(const TerrainTile & dest, const TerrainTile & f
 			!ti->hasBonusOfType(Bonus::NO_TERRAIN_PENALTY, from.terType->getIndex())) //no special movement bonus
 	{
 
-		ret = VLC->heroh->terrCosts[from.terType->getId()];
+		ret = VLC->terrainTypeHandler->getById(dest.terType->getId())->moveCost;
 		ret -= ti->valOfBonuses(Bonus::ROUGH_TERRAIN_DISCOUNT);
 		if(ret < GameConstants::BASE_MOVEMENT_COST)
 			ret = GameConstants::BASE_MOVEMENT_COST;

+ 3 - 3
lib/mapObjects/CGMarket.cpp

@@ -279,10 +279,10 @@ void CGBlackMarket::newTurn(CRandomGenerator & rand) const
 {
 	int resetPeriod = VLC->settings()->getInteger(EGameSettings::MARKETS_BLACK_MARKET_RESTOCK_PERIOD);
 
-	if(resetPeriod == 0) //check if feature changing OH3 behavior is enabled
-		return;
+	bool isFirstDay = cb->getDate(Date::DAY) == 1;
+	bool regularResetTriggered = resetPeriod != 0 && ((cb->getDate(Date::DAY)-1) % resetPeriod) != 0;
 
-	if (((cb->getDate(Date::DAY)-1) % resetPeriod) != 0)
+	if (!isFirstDay && !regularResetTriggered)
 		return;
 
 	SetAvailableArtifacts saa;

+ 8 - 7
server/CGameHandler.cpp

@@ -676,12 +676,12 @@ void CGameHandler::endBattleConfirm(const BattleInfo * battleInfo)
 					sendMoveArtifact(art, &ma);
 				}
 			}
-			while(!finishingBattle->loserHero->artifactsInBackpack.empty())
+			for(int slotNumber = finishingBattle->loserHero->artifactsInBackpack.size() - 1; slotNumber >= 0; slotNumber--)
 			{
 				//we assume that no big artifacts can be found
 				MoveArtifact ma;
 				ma.src = ArtifactLocation(finishingBattle->loserHero,
-										  ArtifactPosition(GameConstants::BACKPACK_START)); //backpack automatically shifts arts to beginning
+					ArtifactPosition(GameConstants::BACKPACK_START + slotNumber)); //backpack automatically shifts arts to beginning
 				const CArtifactInstance * art =  ma.src.getArt();
 				if (art->artType->getId() != ArtifactID::GRAIL) //grail may not be won
 				{
@@ -6517,9 +6517,9 @@ void CGameHandler::runBattle()
 			if(!removeGhosts.changedStacks.empty())
 				sendAndApply(&removeGhosts);
 
-			//check for bad morale => freeze
+			// check for bad morale => freeze
 			int nextStackMorale = next->MoraleVal();
-			if (nextStackMorale < 0)
+			if(!next->hadMorale && !next->waited() && nextStackMorale < 0)
 			{
 				auto diceSize = VLC->settings()->getVector(EGameSettings::COMBAT_BAD_MORALE_DICE);
 				size_t diceIndex = std::min<size_t>(diceSize.size()-1, -nextStackMorale);
@@ -6705,12 +6705,13 @@ void CGameHandler::runBattle()
 				{
 					//check for good morale
 					nextStackMorale = next->MoraleVal();
-					if(!next->hadMorale  //only one extra move per turn possible
+					if( !battleResult.get()
+						&& !next->hadMorale
 						&& !next->defending
 						&& !next->waited()
 						&& !next->fear
-						&&  next->alive()
-						&&  nextStackMorale > 0)
+						&& next->alive()
+						&& nextStackMorale > 0)
 					{
 						auto diceSize = VLC->settings()->getVector(EGameSettings::COMBAT_GOOD_MORALE_DICE);
 						size_t diceIndex = std::min<size_t>(diceSize.size()-1, nextStackMorale);