소스 검색

Merge remote-tracking branch 'upstream/develop' into develop

Xilmi 1 년 전
부모
커밋
987a51cccb

+ 58 - 24
AI/BattleAI/BattleEvaluator.cpp

@@ -66,7 +66,6 @@ BattleEvaluator::BattleEvaluator(
 	damageCache.buildDamageCache(hb, side);
 
 	targets = std::make_unique<PotentialTargets>(activeStack, damageCache, hb);
-	cachedScore = EvaluationResult::INEFFECTIVE_SCORE;
 }
 
 BattleEvaluator::BattleEvaluator(
@@ -85,7 +84,6 @@ BattleEvaluator::BattleEvaluator(
 	damageCache(damageCache), strengthRatio(strengthRatio), battleID(battleID), simulationTurnsCount(simulationTurnsCount)
 {
 	targets = std::make_unique<PotentialTargets>(activeStack, damageCache, hb);
-	cachedScore = EvaluationResult::INEFFECTIVE_SCORE;
 }
 
 std::vector<BattleHex> BattleEvaluator::getBrokenWallMoatHexes() const
@@ -178,8 +176,10 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
 		auto evaluationResult = scoreEvaluator.findBestTarget(stack, *targets, damageCache, hb);
 		auto & bestAttack = evaluationResult.bestAttack;
 
-		cachedAttack = bestAttack;
-		cachedScore = evaluationResult.score;
+		cachedAttack.ap = bestAttack;
+		cachedAttack.score = evaluationResult.score;
+		cachedAttack.turn = 0;
+		cachedAttack.waited = evaluationResult.wait;
 
 		//TODO: consider more complex spellcast evaluation, f.e. because "re-retaliation" during enemy move in same turn for melee attack etc.
 		if(bestSpellcast.has_value() && bestSpellcast->value > bestAttack.damageDiff())
@@ -239,8 +239,9 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
 	if(moveTarget.score > score)
 	{
 		score = moveTarget.score;
-		cachedAttack = moveTarget.cachedAttack;
-		cachedScore = score;
+		cachedAttack.ap = moveTarget.cachedAttack;
+		cachedAttack.score = score;
+		cachedAttack.turn = moveTarget.turnsToRich;
 
 		if(stack->waited())
 		{
@@ -255,6 +256,8 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
 		}
 		else
 		{
+			cachedAttack.waited = true;
+
 			return BattleAction::makeWait(stack);
 		}
 	}
@@ -627,7 +630,15 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
 				auto & ps = possibleCasts[i];
 
 #if BATTLE_TRACE_LEVEL >= 1
-				logAi->trace("Evaluating %s to %d", ps.spell->getNameTranslated(), ps.dest.at(0).hexValue.hex );
+				if(ps.dest.empty())
+					logAi->trace("Evaluating %s", ps.spell->getNameTranslated());
+				else
+				{
+					auto psFirst = ps.dest.front();
+					auto strWhere = psFirst.unitValue ? psFirst.unitValue->getDescription() : std::to_string(psFirst.hexValue.hex);
+
+					logAi->trace("Evaluating %s at %s", ps.spell->getNameTranslated(), strWhere);
+				}
 #endif
 
 				auto state = std::make_shared<HypotheticBattle>(env.get(), cb->getBattle(battleID));
@@ -645,9 +656,15 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
 
 				DamageCache safeCopy = damageCache;
 				DamageCache innerCache(&safeCopy);
+
 				innerCache.buildDamageCache(state, side);
 
-				if(needFullEval || !cachedAttack)
+				if(cachedAttack.ap && cachedAttack.waited)
+				{
+					state->makeWait(activeStack);
+				}
+
+				if(needFullEval || !cachedAttack.ap)
 				{
 #if BATTLE_TRACE_LEVEL >= 1
 					logAi->trace("Full evaluation is started due to stack speed affected.");
@@ -656,22 +673,34 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
 					PotentialTargets innerTargets(activeStack, innerCache, state);
 					BattleExchangeEvaluator innerEvaluator(state, env, strengthRatio, simulationTurnsCount);
 
+					innerEvaluator.updateReachabilityMap(state);
+
+					auto moveTarget = innerEvaluator.findMoveTowardsUnreachable(activeStack, innerTargets, innerCache, state);
+
 					if(!innerTargets.possibleAttacks.empty())
 					{
-						innerEvaluator.updateReachabilityMap(state);
-
 						auto newStackAction = innerEvaluator.findBestTarget(activeStack, innerTargets, innerCache, state);
 
-						ps.value = newStackAction.score;
+						ps.value = std::max(moveTarget.score, newStackAction.score);
 					}
 					else
 					{
-						ps.value = 0;
+						ps.value = moveTarget.score;
 					}
 				}
 				else
 				{
-					ps.value = scoreEvaluator.evaluateExchange(*cachedAttack, 0, *targets, innerCache, state);
+					auto updatedAttacker = state->getForUpdate(cachedAttack.ap->attack.attacker->unitId());
+					auto updatedDefender = state->getForUpdate(cachedAttack.ap->attack.defender->unitId());
+					auto updatedBai = BattleAttackInfo(
+						updatedAttacker.get(),
+						updatedDefender.get(),
+						cachedAttack.ap->attack.chargeDistance,
+						cachedAttack.ap->attack.shooting);
+
+					auto updatedAttack = AttackPossibility::evaluate(updatedBai, cachedAttack.ap->from, innerCache, state);
+
+					ps.value = scoreEvaluator.evaluateExchange(updatedAttack, cachedAttack.turn, *targets, innerCache, state);
 				}
 
 				//! Some units may be dead alltogether. So if they existed before but not now, we know they were killed by the spell
@@ -730,9 +759,9 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
 				}
 				for(const auto & unit : allUnits)
 				{
-					if (!unit->isValidTarget())
+					if(!unit->isValidTarget(true))
 						continue;
-					
+
 					auto newHealth = unit->getAvailableHealth();
 					auto oldHealth = vstd::find_or(healthOfStack, unit->unitId(), 0); // old health value may not exist for newly summoned units
 
@@ -743,7 +772,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
 
 						auto dpsReduce = AttackPossibility::calculateDamageReduce(
 							nullptr,
-							originalDefender &&  originalDefender->alive() ? originalDefender : unit,
+							originalDefender && originalDefender->alive() ? originalDefender : unit,
 							damage,
 							innerCache,
 							state);
@@ -753,13 +782,18 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
 
 						if(ourUnit * goodEffect == 1)
 						{
-							if(ourUnit && goodEffect && (unit->isClone() || unit->isGhost()))
+							auto isMagical = state->getForUpdate(unit->unitId())->summoned
+								|| unit->isClone()
+								|| unit->isGhost();
+
+							if(ourUnit && goodEffect && isMagical)
 								continue;
 
 							ps.value += dpsReduce * scoreEvaluator.getPositiveEffectMultiplier();
 						}
 						else
-							ps.value -= dpsReduce * scoreEvaluator.getNegativeEffectMultiplier();
+							// discourage AI making collateral damage with spells
+							ps.value -= 4 * dpsReduce * scoreEvaluator.getNegativeEffectMultiplier();
 
 #if BATTLE_TRACE_LEVEL >= 1
 						logAi->trace(
@@ -774,6 +808,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
 #endif
 					}
 				}
+
 #if BATTLE_TRACE_LEVEL >= 1
 				logAi->trace("Total score: %2f", ps.value);
 #endif
@@ -784,13 +819,12 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
 
 	LOGFL("Evaluation took %d ms", timer.getDiff());
 
-	auto pscValue = [](const PossibleSpellcast &ps) -> float
-	{
-		return ps.value;
-	};
-	auto castToPerform = *vstd::maxElementByFun(possibleCasts, pscValue);
+	auto castToPerform = *vstd::maxElementByFun(possibleCasts, [](const PossibleSpellcast & ps) -> float
+		{
+			return ps.value;
+		});
 
-	if(castToPerform.value > cachedScore && castToPerform.value > 0)
+	if(castToPerform.value > cachedAttack.score && !vstd::isAlmostEqual(castToPerform.value, cachedAttack.score))
 	{
 		LOGFL("Best spell is %s (value %d). Will cast.", castToPerform.spell->getNameTranslated() % castToPerform.value);
 		BattleAction spellcast;

+ 9 - 2
AI/BattleAI/BattleEvaluator.h

@@ -22,6 +22,14 @@ VCMI_LIB_NAMESPACE_END
 
 class EnemyInfo;
 
+struct CachedAttack
+{
+	std::optional<AttackPossibility> ap;
+	float score = EvaluationResult::INEFFECTIVE_SCORE;
+	uint8_t turn = 255;
+	bool waited = false;
+};
+
 class BattleEvaluator
 {
 	std::unique_ptr<PotentialTargets> targets;
@@ -30,11 +38,10 @@ class BattleEvaluator
 	std::shared_ptr<CBattleCallback> cb;
 	std::shared_ptr<Environment> env;
 	bool activeActionMade = false;
-	std::optional<AttackPossibility> cachedAttack;
+	CachedAttack cachedAttack;
 	PlayerColor playerID;
 	BattleID battleID;
 	BattleSide side;
-	float cachedScore;
 	DamageCache damageCache;
 	float strengthRatio;
 	int simulationTurnsCount;

+ 6 - 5
AI/BattleAI/BattleExchangeVariant.cpp

@@ -219,9 +219,7 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget(
 
 		auto hbWaited = std::make_shared<HypotheticBattle>(env.get(), hb);
 
-		hbWaited->resetActiveUnit();
-		hbWaited->getForUpdate(activeStack->unitId())->waiting = true;
-		hbWaited->getForUpdate(activeStack->unitId())->waitedThisTurn = true;
+		hbWaited->makeWait(activeStack);
 
 		updateReachabilityMap(hbWaited);
 
@@ -378,11 +376,14 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable(
 				logAi->trace("New high score");
 #endif
 
-				for(BattleHex enemyHex : enemy->getAttackableHexes(activeStack))
+				for(const BattleHex & initialEnemyHex : enemy->getAttackableHexes(activeStack))
 				{
-					while(!flying && dists.distances[enemyHex] > speed)
+					BattleHex enemyHex = initialEnemyHex;
+
+					while(!flying && dists.distances[enemyHex] > speed && dists.predecessors.at(enemyHex).isValid())
 					{
 						enemyHex = dists.predecessors.at(enemyHex);
+
 						if(dists.accessibility[enemyHex] == EAccessibility::ALIVE_STACK)
 						{
 							auto defenderToBypass = hb->battleGetUnitByPos(enemyHex);

+ 9 - 1
AI/BattleAI/StackWithBonuses.cpp

@@ -502,10 +502,18 @@ ServerCallback * HypotheticBattle::getServerCallback()
 	return serverCallback.get();
 }
 
+void HypotheticBattle::makeWait(const battle::Unit * activeStack)
+{
+	auto unit = getForUpdate(activeStack->unitId());
+
+	resetActiveUnit();
+	unit->waiting = true;
+	unit->waitedThisTurn = true;
+}
+
 HypotheticBattle::HypotheticServerCallback::HypotheticServerCallback(HypotheticBattle * owner_)
 	:owner(owner_)
 {
-
 }
 
 void HypotheticBattle::HypotheticServerCallback::complain(const std::string & problem)

+ 2 - 0
AI/BattleAI/StackWithBonuses.h

@@ -164,6 +164,8 @@ public:
 
 	int64_t getTreeVersion() const;
 
+	void makeWait(const battle::Unit * activeStack);
+
 	void resetActiveUnit()
 	{
 		activeUnitId = -1;

+ 2 - 0
client/CMakeLists.txt

@@ -83,6 +83,7 @@ set(client_SRCS
 	media/CSoundHandler.cpp
 	media/CVideoHandler.cpp
 
+	render/AssetGenerator.cpp
 	render/CAnimation.cpp
 	render/CBitmapHandler.cpp
 	render/CDefFile.cpp
@@ -285,6 +286,7 @@ set(client_HEADERS
 	media/ISoundPlayer.h
 	media/IVideoPlayer.h
 
+	render/AssetGenerator.h
 	render/CAnimation.h
 	render/CBitmapHandler.h
 	render/CDefFile.h

+ 10 - 0
client/ClientCommandManager.cpp

@@ -18,6 +18,7 @@
 #include "gui/CGuiHandler.h"
 #include "gui/WindowHandler.h"
 #include "render/IRenderHandler.h"
+#include "render/AssetGenerator.h"
 #include "ClientNetPackVisitors.h"
 #include "../lib/CConfigHandler.h"
 #include "../lib/gameState/CGameState.h"
@@ -502,6 +503,12 @@ void ClientCommandManager::handleVsLog(std::istringstream & singleWordBuffer)
 	logVisual->setKey(key);
 }
 
+void ClientCommandManager::handleGenerateAssets()
+{
+	AssetGenerator::generateAll();
+	printCommandMessage("All assets generated");
+}
+
 void ClientCommandManager::printCommandMessage(const std::string &commandMessage, ELogLevel::ELogLevel messageType)
 {
 	switch(messageType)
@@ -624,6 +631,9 @@ void ClientCommandManager::processCommand(const std::string & message, bool call
 	else if(commandName == "vslog")
 		handleVsLog(singleWordBuffer);
 
+	else if(message=="generate assets")
+		handleGenerateAssets();
+
 	else
 	{
 		if (!commandName.empty() && !vstd::iswithin(commandName[0], 0, ' ')) // filter-out debugger/IDE noise

+ 3 - 0
client/ClientCommandManager.h

@@ -84,6 +84,9 @@ class ClientCommandManager //take mantis #2292 issue about account if thinking a
 	// shows object graph
 	void handleVsLog(std::istringstream & singleWordBuffer);
 
+	// generate all assets
+	void handleGenerateAssets();
+
 	// Prints in Chat the given message
 	void printCommandMessage(const std::string &commandMessage, ELogLevel::ELogLevel messageType = ELogLevel::NOT_SET);
 	void giveTurn(const PlayerColor &color);

+ 3 - 0
client/lobby/OptionsTabBase.cpp

@@ -18,6 +18,7 @@
 #include "../widgets/TextControls.h"
 #include "../CServerHandler.h"
 #include "../CGameInfo.h"
+#include "../render/AssetGenerator.h"
 
 #include "../../lib/StartInfo.h"
 #include "../../lib/texts/CGeneralTextHandler.h"
@@ -68,6 +69,8 @@ std::vector<SimturnsInfo> OptionsTabBase::getSimturnsPresets() const
 
 OptionsTabBase::OptionsTabBase(const JsonPath & configPath)
 {
+	AssetGenerator::createAdventureOptionsCleanBackground();
+
 	recActions = 0;
 
 	auto setTimerPresetCallback = [this](int index){

+ 116 - 0
client/render/AssetGenerator.cpp

@@ -0,0 +1,116 @@
+/*
+ * AssetGenerator.cpp, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#include "StdInc.h"
+#include "AssetGenerator.h"
+
+#include "../gui/CGuiHandler.h"
+#include "../render/IImage.h"
+#include "../render/IImageLoader.h"
+#include "../render/Canvas.h"
+#include "../render/IRenderHandler.h"
+
+#include "../lib/filesystem/Filesystem.h"
+
+void AssetGenerator::generateAll()
+{
+	createBigSpellBook();
+	createAdventureOptionsCleanBackground();
+}
+
+void AssetGenerator::createAdventureOptionsCleanBackground()
+{
+	std::string filename = "data/AdventureOptionsBackgroundClear.bmp";
+
+	if(CResourceHandler::get()->existsResource(ResourcePath(filename))) // overridden by mod, no generation
+		return;
+
+	if(!CResourceHandler::get("local")->createResource(filename))
+		return;
+	ResourcePath savePath(filename, EResType::IMAGE);
+
+	auto res = ImagePath::builtin("ADVOPTBK");
+
+	std::shared_ptr<IImage> img = GH.renderHandler().loadImage(res, EImageBlitMode::OPAQUE);
+
+	Canvas canvas = Canvas(Point(575, 585), CanvasScalingPolicy::IGNORE);
+	canvas.draw(img, Point(0, 0), Rect(0, 0, 575, 585));
+	canvas.draw(img, Point(54, 121), Rect(54, 123, 335, 1));
+	canvas.draw(img, Point(158, 84), Rect(156, 84, 2, 37));
+	canvas.draw(img, Point(234, 84), Rect(232, 84, 2, 37));
+	canvas.draw(img, Point(310, 84), Rect(308, 84, 2, 37));
+	canvas.draw(img, Point(53, 567), Rect(53, 520, 339, 3));
+	canvas.draw(img, Point(53, 520), Rect(53, 264, 339, 47));
+
+	std::shared_ptr<IImage> image = GH.renderHandler().createImage(canvas.getInternalSurface());
+
+	image->exportBitmap(*CResourceHandler::get("local")->getResourceName(savePath));
+}
+
+void AssetGenerator::createBigSpellBook()
+{
+	std::string filename = "data/SpellBookLarge.bmp";
+
+	if(CResourceHandler::get()->existsResource(ResourcePath(filename))) // overridden by mod, no generation
+		return;
+
+	if(!CResourceHandler::get("local")->createResource(filename))
+		return;
+	ResourcePath savePath(filename, EResType::IMAGE);
+
+	auto res = ImagePath::builtin("SpelBack");
+
+	std::shared_ptr<IImage> img = GH.renderHandler().loadImage(res, EImageBlitMode::OPAQUE);
+	Canvas canvas = Canvas(Point(800, 600), CanvasScalingPolicy::IGNORE);
+	// edges
+	canvas.draw(img, Point(0, 0), Rect(15, 38, 90, 45));
+	canvas.draw(img, Point(0, 460), Rect(15, 400, 90, 141));
+	canvas.draw(img, Point(705, 0), Rect(509, 38, 95, 45));
+	canvas.draw(img, Point(705, 460), Rect(509, 400, 95, 141));
+	// left / right
+	Canvas tmp1 = Canvas(Point(90, 355 - 45), CanvasScalingPolicy::IGNORE);
+	tmp1.draw(img, Point(0, 0), Rect(15, 38 + 45, 90, 355 - 45));
+	canvas.drawScaled(tmp1, Point(0, 45), Point(90, 415));
+	Canvas tmp2 = Canvas(Point(95, 355 - 45), CanvasScalingPolicy::IGNORE);
+	tmp2.draw(img, Point(0, 0), Rect(509, 38 + 45, 95, 355 - 45));
+	canvas.drawScaled(tmp2, Point(705, 45), Point(95, 415));
+	// top / bottom
+	Canvas tmp3 = Canvas(Point(409, 45), CanvasScalingPolicy::IGNORE);
+	tmp3.draw(img, Point(0, 0), Rect(100, 38, 409, 45));
+	canvas.drawScaled(tmp3, Point(90, 0), Point(615, 45));
+	Canvas tmp4 = Canvas(Point(409, 141), CanvasScalingPolicy::IGNORE);
+	tmp4.draw(img, Point(0, 0), Rect(100, 400, 409, 141));
+	canvas.drawScaled(tmp4, Point(90, 460), Point(615, 141));
+	// middle
+	Canvas tmp5 = Canvas(Point(409, 141), CanvasScalingPolicy::IGNORE);
+	tmp5.draw(img, Point(0, 0), Rect(100, 38 + 45, 509 - 15, 400 - 38));
+	canvas.drawScaled(tmp5, Point(90, 45), Point(615, 415));
+	// carpet
+	Canvas tmp6 = Canvas(Point(590, 59), CanvasScalingPolicy::IGNORE);
+	tmp6.draw(img, Point(0, 0), Rect(15, 484, 590, 59));
+	canvas.drawScaled(tmp6, Point(0, 545), Point(800, 59));
+	// remove bookmarks
+	for (int i = 0; i < 56; i++)
+		canvas.draw(Canvas(canvas, Rect(i < 30 ? 268 : 327, 464, 1, 46)), Point(269 + i, 464));
+	for (int i = 0; i < 56; i++)
+		canvas.draw(Canvas(canvas, Rect(469, 464, 1, 42)), Point(470 + i, 464));
+	for (int i = 0; i < 57; i++)
+		canvas.draw(Canvas(canvas, Rect(i < 30 ? 564 : 630, 464, 1, 44)), Point(565 + i, 464));
+	for (int i = 0; i < 56; i++)
+		canvas.draw(Canvas(canvas, Rect(656, 464, 1, 47)), Point(657 + i, 464));
+	// draw bookmarks
+	canvas.draw(img, Point(278, 464), Rect(220, 405, 37, 47));
+	canvas.draw(img, Point(481, 465), Rect(354, 406, 37, 41));
+	canvas.draw(img, Point(575, 465), Rect(417, 406, 37, 45));
+	canvas.draw(img, Point(667, 465), Rect(478, 406, 37, 47));
+
+	std::shared_ptr<IImage> image = GH.renderHandler().createImage(canvas.getInternalSurface());
+
+	image->exportBitmap(*CResourceHandler::get("local")->getResourceName(savePath));
+}

+ 18 - 0
client/render/AssetGenerator.h

@@ -0,0 +1,18 @@
+/*
+ * AssetGenerator.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#pragma once
+
+class AssetGenerator
+{
+public:
+    static void generateAll();
+    static void createAdventureOptionsCleanBackground();
+    static void createBigSpellBook();
+};

+ 3 - 54
client/windows/CSpellWindow.cpp

@@ -31,10 +31,7 @@
 #include "../widgets/TextControls.h"
 #include "../widgets/Buttons.h"
 #include "../adventureMap/AdventureMapInterface.h"
-#include "../render/IRenderHandler.h"
-#include "../render/IImage.h"
-#include "../render/IImageLoader.h"
-#include "../render/Canvas.h"
+#include "../render/AssetGenerator.h"
 
 #include "../../CCallback.h"
 
@@ -119,7 +116,8 @@ CSpellWindow::CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _m
 
 	if(isBigSpellbook)
 	{
-		background = std::make_shared<CPicture>(createBigSpellBook(), Point(0, 0));
+		AssetGenerator::createBigSpellBook();
+		background = std::make_shared<CPicture>(ImagePath::builtin("SpellBookLarge"), 0, 0);
 		updateShadow();
 	}
 	else
@@ -221,55 +219,6 @@ CSpellWindow::~CSpellWindow()
 {
 }
 
-std::shared_ptr<IImage> CSpellWindow::createBigSpellBook()
-{
-	std::shared_ptr<IImage> img = GH.renderHandler().loadImage(ImagePath::builtin("SpelBack"), EImageBlitMode::OPAQUE);
-	Canvas canvas = Canvas(Point(800, 600), CanvasScalingPolicy::AUTO);
-	// edges
-	canvas.draw(img, Point(0, 0), Rect(15, 38, 90, 45));
-	canvas.draw(img, Point(0, 460), Rect(15, 400, 90, 141));
-	canvas.draw(img, Point(705, 0), Rect(509, 38, 95, 45));
-	canvas.draw(img, Point(705, 460), Rect(509, 400, 95, 141));
-	// left / right
-	Canvas tmp1 = Canvas(Point(90, 355 - 45), CanvasScalingPolicy::AUTO);
-	tmp1.draw(img, Point(0, 0), Rect(15, 38 + 45, 90, 355 - 45));
-	canvas.drawScaled(tmp1, Point(0, 45), Point(90, 415));
-	Canvas tmp2 = Canvas(Point(95, 355 - 45), CanvasScalingPolicy::AUTO);
-	tmp2.draw(img, Point(0, 0), Rect(509, 38 + 45, 95, 355 - 45));
-	canvas.drawScaled(tmp2, Point(705, 45), Point(95, 415));
-	// top / bottom
-	Canvas tmp3 = Canvas(Point(409, 45), CanvasScalingPolicy::AUTO);
-	tmp3.draw(img, Point(0, 0), Rect(100, 38, 409, 45));
-	canvas.drawScaled(tmp3, Point(90, 0), Point(615, 45));
-	Canvas tmp4 = Canvas(Point(409, 141), CanvasScalingPolicy::AUTO);
-	tmp4.draw(img, Point(0, 0), Rect(100, 400, 409, 141));
-	canvas.drawScaled(tmp4, Point(90, 460), Point(615, 141));
-	// middle
-	Canvas tmp5 = Canvas(Point(409, 141), CanvasScalingPolicy::AUTO);
-	tmp5.draw(img, Point(0, 0), Rect(100, 38 + 45, 509 - 15, 400 - 38));
-	canvas.drawScaled(tmp5, Point(90, 45), Point(615, 415));
-	// carpet
-	Canvas tmp6 = Canvas(Point(590, 59), CanvasScalingPolicy::AUTO);
-	tmp6.draw(img, Point(0, 0), Rect(15, 484, 590, 59));
-	canvas.drawScaled(tmp6, Point(0, 545), Point(800, 59));
-	// remove bookmarks
-	for (int i = 0; i < 56; i++)
-		canvas.draw(Canvas(canvas, Rect(i < 30 ? 268 : 327, 464, 1, 46)), Point(269 + i, 464));
-	for (int i = 0; i < 56; i++)
-		canvas.draw(Canvas(canvas, Rect(469, 464, 1, 42)), Point(470 + i, 464));
-	for (int i = 0; i < 57; i++)
-		canvas.draw(Canvas(canvas, Rect(i < 30 ? 564 : 630, 464, 1, 44)), Point(565 + i, 464));
-	for (int i = 0; i < 56; i++)
-		canvas.draw(Canvas(canvas, Rect(656, 464, 1, 47)), Point(657 + i, 464));
-	// draw bookmarks
-	canvas.draw(img, Point(278, 464), Rect(220, 405, 37, 47));
-	canvas.draw(img, Point(481, 465), Rect(354, 406, 37, 41));
-	canvas.draw(img, Point(575, 465), Rect(417, 406, 37, 45));
-	canvas.draw(img, Point(667, 465), Rect(478, 406, 37, 47));
-
-	return GH.renderHandler().createImage(canvas.getInternalSurface());
-}
-
 void CSpellWindow::searchInput()
 {
 	if(searchBox)

+ 0 - 2
client/windows/CSpellWindow.h

@@ -113,8 +113,6 @@ class CSpellWindow : public CWindowObject
 	void turnPageLeft();
 	void turnPageRight();
 
-	std::shared_ptr<IImage> createBigSpellBook();
-
 	bool openOnBattleSpells;
 	std::function<void(SpellID)> onSpellSelect; //external processing of selected spell
 

+ 1 - 26
config/widgets/extraOptionsTab.json

@@ -6,7 +6,7 @@
 		{
 			"name": "background",
 			"type": "picture",
-			"image": "ADVOPTBK",
+			"image": "AdventureOptionsBackgroundClear",
 			"position": {"x": 0, "y": 6}
 		},
 		{
@@ -35,31 +35,6 @@
 			"rect": {"x": 60, "y": 48, "w": 320, "h": 0},
 			"adoptHeight": true
 		},
-		{
-			"type": "transparentFilledRectangle",
-			"rect": {"x": 54, "y": 127, "w": 335, "h": 2},
-			"color": [24, 41, 90, 255]
-		},
-		{
-			"type": "transparentFilledRectangle",
-			"rect": {"x": 159, "y": 90, "w": 2, "h": 38},
-			"color": [24, 41, 90, 255]
-		},
-		{
-			"type": "transparentFilledRectangle",
-			"rect": {"x": 235, "y": 90, "w": 2, "h": 38},
-			"color": [24, 41, 90, 255]
-		},
-		{
-			"type": "transparentFilledRectangle",
-			"rect": {"x": 311, "y": 90, "w": 2, "h": 38},
-			"color": [24, 41, 90, 255]
-		},
-		{
-			"type": "transparentFilledRectangle",
-			"rect": {"x": 55, "y": 556, "w": 335, "h": 19},
-			"color": [24, 41, 90, 255]
-		},
 		{
 			"name": "ExtraOptionsButtons",
 			"type" : "verticalLayout",

+ 8 - 7
docs/players/Cheat_Codes.md

@@ -120,13 +120,14 @@ Below a list of supported commands, with their arguments wrapped in `<>`
 `bonuses` - shows bonuses of currently selected adventure map object
 
 #### Extract commands
-`translate` - save game texts into json files  
-`translate maps` - save map and campaign texts into json files  
-`get config` - save game objects data into json files  
-`get scripts` - dumps lua script stuff into files (currently inactive due to scripting disabled for default builds)    
-`get txt` - save game texts into .txt files matching original heroes 3 files  
-`def2bmp <.def file name>` - extract .def animation as BMP files  
-`extract <relative file path>` - export file into directory used by other extraction commands  
+`translate` - save game texts into json files
+`translate maps` - save map and campaign texts into json files
+`get config` - save game objects data into json files
+`get scripts` - dumps lua script stuff into files (currently inactive due to scripting disabled for default builds)
+`get txt` - save game texts into .txt files matching original heroes 3 files
+`def2bmp <.def file name>` - extract .def animation as BMP files
+`extract <relative file path>` - export file into directory used by other extraction commands
+`generate assets` - generate all assets at once
 
 #### AI commands
 `setBattleAI <ai name>` - change battle AI used by neutral creatures to the one specified, persists through game quit  

+ 1 - 1
docs/players/Installation_iOS.md

@@ -14,7 +14,7 @@ have the following options:
 - if you're on iOS 14.0-15.4.1, you can try <https://github.com/opa334/TrollStore>
 - Get signer tool [here](https://dantheman827.github.io/ios-app-signer/) and a guide [here](https://forum.kodi.tv/showthread.php?tid=245978) (it's for Kodi, but the logic is the same). Signing with this app can only be done on macOS.
 - [Create signing assets on macOS from terminal](https://github.com/kambala-decapitator/xcode-auto-signing-assets). In the command replace `your.bundle.id` with something like `com.MY-NAME.vcmi`. After that use the above signer tool.
-- [Sign from any OS](https://github.com/indygreg/PyOxidizer/tree/main/tugger-code-signing). You'd still need to find a way to create signing assets (private key and provisioning profile) though.
+- [Sign from any OS (Rust)](https://github.com/indygreg/PyOxidizer/tree/main/tugger-code-signing) / [alternative project (C++)](https://github.com/zhlynn/zsign). You'd still need to find a way to create signing assets (private key and provisioning profile) though.
 
 To install the signed ipa on your device, you can use Xcode or Apple Configurator (available on the Mac App Store for free). The latter also allows installing ipa from the command line, here's an example that assumes you have only 1 device connected to your Mac and the signed ipa is on your desktop:
 

+ 1 - 1
lib/CPlayerState.h

@@ -104,7 +104,7 @@ public:
 
 	bool checkVanquished() const
 	{
-		return ownedObjects.empty();
+		return getHeroes().empty() && getTowns().empty();
 	}
 
 	template <typename Handler> void serialize(Handler &h)

+ 0 - 2
lib/filesystem/AdapterLoaders.cpp

@@ -68,10 +68,8 @@ std::unique_ptr<CInputStream> CFilesystemList::load(const ResourcePath & resourc
 {
 	// load resource from last loader that have it (last overridden version)
 	for(const auto & loader : boost::adaptors::reverse(loaders))
-	{
 		if (loader->existsResource(resourceName))
 			return loader->load(resourceName);
-	}
 
 	throw std::runtime_error("Resource with name " + resourceName.getName() + " and type "
 		+ EResTypeHelper::getEResTypeAsString(resourceName.getType()) + " wasn't found.");

+ 3 - 2
lib/filesystem/CFilesystemLoader.cpp

@@ -118,10 +118,11 @@ std::unordered_map<ResourcePath, boost::filesystem::path> CFilesystemLoader::lis
 		EResType::ARCHIVE_SND,
 		EResType::ARCHIVE_ZIP };
 	static const std::set<EResType> initialTypes(initArray, initArray + std::size(initArray));
-
-	assert(boost::filesystem::is_directory(baseDirectory));
 	std::unordered_map<ResourcePath, boost::filesystem::path> fileList;
 
+	if(!boost::filesystem::is_directory(baseDirectory))
+		return fileList;
+
 	std::vector<boost::filesystem::path> path; //vector holding relative path to our file
 
 	boost::filesystem::recursive_directory_iterator enddir;

+ 7 - 0
lib/filesystem/Filesystem.cpp

@@ -183,9 +183,16 @@ void CResourceHandler::initialize()
 	knownLoaders["saves"] = new CFilesystemLoader("SAVES/", VCMIDirs::get().userSavePath());
 	knownLoaders["config"] = new CFilesystemLoader("CONFIG/", VCMIDirs::get().userConfigPath());
 
+	if(boost::filesystem::is_directory(VCMIDirs::get().userDataPath() / "Generated"))
+		boost::filesystem::remove_all(VCMIDirs::get().userDataPath() / "Generated");
+	knownLoaders["gen_data"] = new CFilesystemLoader("DATA/", VCMIDirs::get().userDataPath() / "Generated" / "Data");
+	knownLoaders["gen_sprites"] = new CFilesystemLoader("SPRITES/", VCMIDirs::get().userDataPath() / "Generated" / "Sprites");
+
 	auto * localFS = new CFilesystemList();
 	localFS->addLoader(knownLoaders["saves"], true);
 	localFS->addLoader(knownLoaders["config"], true);
+	localFS->addLoader(knownLoaders["gen_data"], true);
+	localFS->addLoader(knownLoaders["gen_sprites"], true);
 
 	addFilesystem("root", "initial", createInitial());
 	addFilesystem("root", "data", new CFilesystemList());

+ 6 - 3
server/CGameHandler.cpp

@@ -1066,10 +1066,13 @@ void CGameHandler::setOwner(const CGObjectInstance * obj, const PlayerColor owne
 
 	if ((obj->ID == Obj::CREATURE_GENERATOR1 || obj->ID == Obj::CREATURE_GENERATOR4))
 	{
-		for (const CGTownInstance * t : getPlayerState(owner)->getTowns())
+		if (owner.isValidPlayer())
 		{
-			if (t->hasBuilt(BuildingSubID::PORTAL_OF_SUMMONING))
-				setPortalDwelling(t);//set initial creatures for all portals of summoning
+			for (const CGTownInstance * t : getPlayerState(owner)->getTowns())
+			{
+				if (t->hasBuilt(BuildingSubID::PORTAL_OF_SUMMONING))
+					setPortalDwelling(t);//set initial creatures for all portals of summoning
+			}
 		}
 	}
 }