瀏覽代碼

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);
 	damageCache.buildDamageCache(hb, side);
 
 
 	targets = std::make_unique<PotentialTargets>(activeStack, damageCache, hb);
 	targets = std::make_unique<PotentialTargets>(activeStack, damageCache, hb);
-	cachedScore = EvaluationResult::INEFFECTIVE_SCORE;
 }
 }
 
 
 BattleEvaluator::BattleEvaluator(
 BattleEvaluator::BattleEvaluator(
@@ -85,7 +84,6 @@ BattleEvaluator::BattleEvaluator(
 	damageCache(damageCache), strengthRatio(strengthRatio), battleID(battleID), simulationTurnsCount(simulationTurnsCount)
 	damageCache(damageCache), strengthRatio(strengthRatio), battleID(battleID), simulationTurnsCount(simulationTurnsCount)
 {
 {
 	targets = std::make_unique<PotentialTargets>(activeStack, damageCache, hb);
 	targets = std::make_unique<PotentialTargets>(activeStack, damageCache, hb);
-	cachedScore = EvaluationResult::INEFFECTIVE_SCORE;
 }
 }
 
 
 std::vector<BattleHex> BattleEvaluator::getBrokenWallMoatHexes() const
 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 evaluationResult = scoreEvaluator.findBestTarget(stack, *targets, damageCache, hb);
 		auto & bestAttack = evaluationResult.bestAttack;
 		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.
 		//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())
 		if(bestSpellcast.has_value() && bestSpellcast->value > bestAttack.damageDiff())
@@ -239,8 +239,9 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
 	if(moveTarget.score > score)
 	if(moveTarget.score > score)
 	{
 	{
 		score = moveTarget.score;
 		score = moveTarget.score;
-		cachedAttack = moveTarget.cachedAttack;
-		cachedScore = score;
+		cachedAttack.ap = moveTarget.cachedAttack;
+		cachedAttack.score = score;
+		cachedAttack.turn = moveTarget.turnsToRich;
 
 
 		if(stack->waited())
 		if(stack->waited())
 		{
 		{
@@ -255,6 +256,8 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
 		}
 		}
 		else
 		else
 		{
 		{
+			cachedAttack.waited = true;
+
 			return BattleAction::makeWait(stack);
 			return BattleAction::makeWait(stack);
 		}
 		}
 	}
 	}
@@ -627,7 +630,15 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
 				auto & ps = possibleCasts[i];
 				auto & ps = possibleCasts[i];
 
 
 #if BATTLE_TRACE_LEVEL >= 1
 #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
 #endif
 
 
 				auto state = std::make_shared<HypotheticBattle>(env.get(), cb->getBattle(battleID));
 				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 safeCopy = damageCache;
 				DamageCache innerCache(&safeCopy);
 				DamageCache innerCache(&safeCopy);
+
 				innerCache.buildDamageCache(state, side);
 				innerCache.buildDamageCache(state, side);
 
 
-				if(needFullEval || !cachedAttack)
+				if(cachedAttack.ap && cachedAttack.waited)
+				{
+					state->makeWait(activeStack);
+				}
+
+				if(needFullEval || !cachedAttack.ap)
 				{
 				{
 #if BATTLE_TRACE_LEVEL >= 1
 #if BATTLE_TRACE_LEVEL >= 1
 					logAi->trace("Full evaluation is started due to stack speed affected.");
 					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);
 					PotentialTargets innerTargets(activeStack, innerCache, state);
 					BattleExchangeEvaluator innerEvaluator(state, env, strengthRatio, simulationTurnsCount);
 					BattleExchangeEvaluator innerEvaluator(state, env, strengthRatio, simulationTurnsCount);
 
 
+					innerEvaluator.updateReachabilityMap(state);
+
+					auto moveTarget = innerEvaluator.findMoveTowardsUnreachable(activeStack, innerTargets, innerCache, state);
+
 					if(!innerTargets.possibleAttacks.empty())
 					if(!innerTargets.possibleAttacks.empty())
 					{
 					{
-						innerEvaluator.updateReachabilityMap(state);
-
 						auto newStackAction = innerEvaluator.findBestTarget(activeStack, innerTargets, innerCache, state);
 						auto newStackAction = innerEvaluator.findBestTarget(activeStack, innerTargets, innerCache, state);
 
 
-						ps.value = newStackAction.score;
+						ps.value = std::max(moveTarget.score, newStackAction.score);
 					}
 					}
 					else
 					else
 					{
 					{
-						ps.value = 0;
+						ps.value = moveTarget.score;
 					}
 					}
 				}
 				}
 				else
 				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
 				//! 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)
 				for(const auto & unit : allUnits)
 				{
 				{
-					if (!unit->isValidTarget())
+					if(!unit->isValidTarget(true))
 						continue;
 						continue;
-					
+
 					auto newHealth = unit->getAvailableHealth();
 					auto newHealth = unit->getAvailableHealth();
 					auto oldHealth = vstd::find_or(healthOfStack, unit->unitId(), 0); // old health value may not exist for newly summoned units
 					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(
 						auto dpsReduce = AttackPossibility::calculateDamageReduce(
 							nullptr,
 							nullptr,
-							originalDefender &&  originalDefender->alive() ? originalDefender : unit,
+							originalDefender && originalDefender->alive() ? originalDefender : unit,
 							damage,
 							damage,
 							innerCache,
 							innerCache,
 							state);
 							state);
@@ -753,13 +782,18 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
 
 
 						if(ourUnit * goodEffect == 1)
 						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;
 								continue;
 
 
 							ps.value += dpsReduce * scoreEvaluator.getPositiveEffectMultiplier();
 							ps.value += dpsReduce * scoreEvaluator.getPositiveEffectMultiplier();
 						}
 						}
 						else
 						else
-							ps.value -= dpsReduce * scoreEvaluator.getNegativeEffectMultiplier();
+							// discourage AI making collateral damage with spells
+							ps.value -= 4 * dpsReduce * scoreEvaluator.getNegativeEffectMultiplier();
 
 
 #if BATTLE_TRACE_LEVEL >= 1
 #if BATTLE_TRACE_LEVEL >= 1
 						logAi->trace(
 						logAi->trace(
@@ -774,6 +808,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
 #endif
 #endif
 					}
 					}
 				}
 				}
+
 #if BATTLE_TRACE_LEVEL >= 1
 #if BATTLE_TRACE_LEVEL >= 1
 				logAi->trace("Total score: %2f", ps.value);
 				logAi->trace("Total score: %2f", ps.value);
 #endif
 #endif
@@ -784,13 +819,12 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
 
 
 	LOGFL("Evaluation took %d ms", timer.getDiff());
 	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);
 		LOGFL("Best spell is %s (value %d). Will cast.", castToPerform.spell->getNameTranslated() % castToPerform.value);
 		BattleAction spellcast;
 		BattleAction spellcast;

+ 9 - 2
AI/BattleAI/BattleEvaluator.h

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

+ 6 - 5
AI/BattleAI/BattleExchangeVariant.cpp

@@ -219,9 +219,7 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget(
 
 
 		auto hbWaited = std::make_shared<HypotheticBattle>(env.get(), hb);
 		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);
 		updateReachabilityMap(hbWaited);
 
 
@@ -378,11 +376,14 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable(
 				logAi->trace("New high score");
 				logAi->trace("New high score");
 #endif
 #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);
 						enemyHex = dists.predecessors.at(enemyHex);
+
 						if(dists.accessibility[enemyHex] == EAccessibility::ALIVE_STACK)
 						if(dists.accessibility[enemyHex] == EAccessibility::ALIVE_STACK)
 						{
 						{
 							auto defenderToBypass = hb->battleGetUnitByPos(enemyHex);
 							auto defenderToBypass = hb->battleGetUnitByPos(enemyHex);

+ 9 - 1
AI/BattleAI/StackWithBonuses.cpp

@@ -502,10 +502,18 @@ ServerCallback * HypotheticBattle::getServerCallback()
 	return serverCallback.get();
 	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_)
 HypotheticBattle::HypotheticServerCallback::HypotheticServerCallback(HypotheticBattle * owner_)
 	:owner(owner_)
 	:owner(owner_)
 {
 {
-
 }
 }
 
 
 void HypotheticBattle::HypotheticServerCallback::complain(const std::string & problem)
 void HypotheticBattle::HypotheticServerCallback::complain(const std::string & problem)

+ 2 - 0
AI/BattleAI/StackWithBonuses.h

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

+ 2 - 0
client/CMakeLists.txt

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

+ 10 - 0
client/ClientCommandManager.cpp

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

+ 3 - 0
client/lobby/OptionsTabBase.cpp

@@ -18,6 +18,7 @@
 #include "../widgets/TextControls.h"
 #include "../widgets/TextControls.h"
 #include "../CServerHandler.h"
 #include "../CServerHandler.h"
 #include "../CGameInfo.h"
 #include "../CGameInfo.h"
+#include "../render/AssetGenerator.h"
 
 
 #include "../../lib/StartInfo.h"
 #include "../../lib/StartInfo.h"
 #include "../../lib/texts/CGeneralTextHandler.h"
 #include "../../lib/texts/CGeneralTextHandler.h"
@@ -68,6 +69,8 @@ std::vector<SimturnsInfo> OptionsTabBase::getSimturnsPresets() const
 
 
 OptionsTabBase::OptionsTabBase(const JsonPath & configPath)
 OptionsTabBase::OptionsTabBase(const JsonPath & configPath)
 {
 {
+	AssetGenerator::createAdventureOptionsCleanBackground();
+
 	recActions = 0;
 	recActions = 0;
 
 
 	auto setTimerPresetCallback = [this](int index){
 	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/TextControls.h"
 #include "../widgets/Buttons.h"
 #include "../widgets/Buttons.h"
 #include "../adventureMap/AdventureMapInterface.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"
 #include "../../CCallback.h"
 
 
@@ -119,7 +116,8 @@ CSpellWindow::CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _m
 
 
 	if(isBigSpellbook)
 	if(isBigSpellbook)
 	{
 	{
-		background = std::make_shared<CPicture>(createBigSpellBook(), Point(0, 0));
+		AssetGenerator::createBigSpellBook();
+		background = std::make_shared<CPicture>(ImagePath::builtin("SpellBookLarge"), 0, 0);
 		updateShadow();
 		updateShadow();
 	}
 	}
 	else
 	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()
 void CSpellWindow::searchInput()
 {
 {
 	if(searchBox)
 	if(searchBox)

+ 0 - 2
client/windows/CSpellWindow.h

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

+ 1 - 26
config/widgets/extraOptionsTab.json

@@ -6,7 +6,7 @@
 		{
 		{
 			"name": "background",
 			"name": "background",
 			"type": "picture",
 			"type": "picture",
-			"image": "ADVOPTBK",
+			"image": "AdventureOptionsBackgroundClear",
 			"position": {"x": 0, "y": 6}
 			"position": {"x": 0, "y": 6}
 		},
 		},
 		{
 		{
@@ -35,31 +35,6 @@
 			"rect": {"x": 60, "y": 48, "w": 320, "h": 0},
 			"rect": {"x": 60, "y": 48, "w": 320, "h": 0},
 			"adoptHeight": true
 			"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",
 			"name": "ExtraOptionsButtons",
 			"type" : "verticalLayout",
 			"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
 `bonuses` - shows bonuses of currently selected adventure map object
 
 
 #### Extract commands
 #### 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
 #### AI commands
 `setBattleAI <ai name>` - change battle AI used by neutral creatures to the one specified, persists through game quit  
 `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>
 - 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.
 - 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.
 - [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:
 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
 	bool checkVanquished() const
 	{
 	{
-		return ownedObjects.empty();
+		return getHeroes().empty() && getTowns().empty();
 	}
 	}
 
 
 	template <typename Handler> void serialize(Handler &h)
 	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)
 	// load resource from last loader that have it (last overridden version)
 	for(const auto & loader : boost::adaptors::reverse(loaders))
 	for(const auto & loader : boost::adaptors::reverse(loaders))
-	{
 		if (loader->existsResource(resourceName))
 		if (loader->existsResource(resourceName))
 			return loader->load(resourceName);
 			return loader->load(resourceName);
-	}
 
 
 	throw std::runtime_error("Resource with name " + resourceName.getName() + " and type "
 	throw std::runtime_error("Resource with name " + resourceName.getName() + " and type "
 		+ EResTypeHelper::getEResTypeAsString(resourceName.getType()) + " wasn't found.");
 		+ 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_SND,
 		EResType::ARCHIVE_ZIP };
 		EResType::ARCHIVE_ZIP };
 	static const std::set<EResType> initialTypes(initArray, initArray + std::size(initArray));
 	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;
 	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
 	std::vector<boost::filesystem::path> path; //vector holding relative path to our file
 
 
 	boost::filesystem::recursive_directory_iterator enddir;
 	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["saves"] = new CFilesystemLoader("SAVES/", VCMIDirs::get().userSavePath());
 	knownLoaders["config"] = new CFilesystemLoader("CONFIG/", VCMIDirs::get().userConfigPath());
 	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();
 	auto * localFS = new CFilesystemList();
 	localFS->addLoader(knownLoaders["saves"], true);
 	localFS->addLoader(knownLoaders["saves"], true);
 	localFS->addLoader(knownLoaders["config"], true);
 	localFS->addLoader(knownLoaders["config"], true);
+	localFS->addLoader(knownLoaders["gen_data"], true);
+	localFS->addLoader(knownLoaders["gen_sprites"], true);
 
 
 	addFilesystem("root", "initial", createInitial());
 	addFilesystem("root", "initial", createInitial());
 	addFilesystem("root", "data", new CFilesystemList());
 	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))
 	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
+			}
 		}
 		}
 	}
 	}
 }
 }