From 38bb5a76e690e0a61108b7eafab8df176fe9828a Mon Sep 17 00:00:00 2001
From: Ivan Savenko <saven.ivan@gmail.com>
Date: Mon, 3 Feb 2025 11:44:21 +0000
Subject: [PATCH] Handle flag color overlay and creature selection overlay
 separately

Fixes handling of Iron Golem animation from HotA
---
 client/battle/CreatureAnimation.cpp |  4 +-
 client/mapView/MapRenderer.cpp      |  2 +-
 client/render/IImage.h              | 12 ++--
 client/renderSDL/RenderHandler.cpp  |  8 ++-
 client/renderSDL/ScalableImage.cpp  | 92 +++++++++++++++++++----------
 client/renderSDL/ScalableImage.h    |  2 +-
 client/widgets/Images.cpp           |  6 +-
 7 files changed, 82 insertions(+), 44 deletions(-)

diff --git a/client/battle/CreatureAnimation.cpp b/client/battle/CreatureAnimation.cpp
index 31ab870cf..60a59cf54 100644
--- a/client/battle/CreatureAnimation.cpp
+++ b/client/battle/CreatureAnimation.cpp
@@ -200,8 +200,8 @@ CreatureAnimation::CreatureAnimation(const AnimationPath & name_, TSpeedControll
 	  speedController(controller),
 	  once(false)
 {
-	forward = GH.renderHandler().loadAnimation(name_, EImageBlitMode::WITH_SHADOW_AND_OVERLAY);
-	reverse = GH.renderHandler().loadAnimation(name_, EImageBlitMode::WITH_SHADOW_AND_OVERLAY);
+	forward = GH.renderHandler().loadAnimation(name_, EImageBlitMode::WITH_SHADOW_AND_SELECTION);
+	reverse = GH.renderHandler().loadAnimation(name_, EImageBlitMode::WITH_SHADOW_AND_SELECTION);
 
 	// if necessary, add one frame into vcmi-only group DEAD
 	if(forward->size(size_t(ECreatureAnimType::DEAD)) == 0)
diff --git a/client/mapView/MapRenderer.cpp b/client/mapView/MapRenderer.cpp
index d3c0a88c7..c43ee60f1 100644
--- a/client/mapView/MapRenderer.cpp
+++ b/client/mapView/MapRenderer.cpp
@@ -407,7 +407,7 @@ std::shared_ptr<CAnimation> MapRendererObjects::getAnimation(const AnimationPath
 	if(it != animations.end())
 		return it->second;
 
-	auto ret = GH.renderHandler().loadAnimation(filename, enableOverlay ? EImageBlitMode::WITH_SHADOW_AND_OVERLAY : EImageBlitMode::WITH_SHADOW);
+	auto ret = GH.renderHandler().loadAnimation(filename, enableOverlay ? EImageBlitMode::WITH_SHADOW_AND_FLAG_COLOR: EImageBlitMode::WITH_SHADOW);
 	animations[filename] = ret;
 
 	if(generateMovementGroups)
diff --git a/client/render/IImage.h b/client/render/IImage.h
index ec27276a6..9c2216553 100644
--- a/client/render/IImage.h
+++ b/client/render/IImage.h
@@ -47,19 +47,23 @@ enum class EImageBlitMode : uint8_t
 	WITH_SHADOW,
 
 	/// RGBA, may consist from 3 separate parts: base, shadow, and overlay
-	WITH_SHADOW_AND_OVERLAY,
+	WITH_SHADOW_AND_SELECTION,
+	WITH_SHADOW_AND_FLAG_COLOR,
 
 	/// RGBA, contains only body, with shadow and overlay disabled
-	ONLY_BODY,
+	ONLY_BODY_HIDE_SELECTION,
+	ONLY_BODY_HIDE_FLAG_COLOR,
 
 	/// RGBA, contains only body, with shadow disabled and overlay treated as part of body
 	ONLY_BODY_IGNORE_OVERLAY,
 
 	/// RGBA, contains only shadow
-	ONLY_SHADOW,
+	ONLY_SHADOW_HIDE_SELECTION,
+	ONLY_SHADOW_HIDE_FLAG_COLOR,
 
 	/// RGBA, contains only overlay
-	ONLY_OVERLAY,
+	ONLY_SELECTION,
+	ONLY_FLAG_COLOR,
 };
 
 enum class EScalingAlgorithm : int8_t
diff --git a/client/renderSDL/RenderHandler.cpp b/client/renderSDL/RenderHandler.cpp
index 92469f4d3..aa771c3e9 100644
--- a/client/renderSDL/RenderHandler.cpp
+++ b/client/renderSDL/RenderHandler.cpp
@@ -230,6 +230,7 @@ std::shared_ptr<ISharedImage> RenderHandler::loadImageFromFileUncached(const Ima
 		if (generated)
 			return generated;
 
+		logGlobal->error("Failed to load image %s", locator.image->getOriginalName());
 		return std::make_shared<SDLImageShared>(ImagePath::builtin("DEFAULT"));
 	}
 
@@ -292,9 +293,9 @@ std::shared_ptr<SDLImageShared> RenderHandler::loadScaledImage(const ImageLocato
 
 	std::string imagePathString = pathToLoad.getName();
 
-	if(locator.layer == EImageBlitMode::ONLY_OVERLAY)
+	if(locator.layer == EImageBlitMode::ONLY_FLAG_COLOR || locator.layer == EImageBlitMode::ONLY_SELECTION)
 		imagePathString += "-OVERLAY";
-	if(locator.layer == EImageBlitMode::ONLY_SHADOW)
+	if(locator.layer == EImageBlitMode::ONLY_SHADOW_HIDE_SELECTION || locator.layer == EImageBlitMode::ONLY_SHADOW_HIDE_FLAG_COLOR)
 		imagePathString += "-SHADOW";
 	if(locator.playerColored.isValidPlayer())
 		imagePathString += "-" + boost::to_upper_copy(GameConstants::PLAYER_COLOR_NAMES[locator.playerColored.getNum()]);
@@ -347,7 +348,10 @@ std::shared_ptr<IImage> RenderHandler::loadImage(const AnimationPath & path, int
 	if (!locator.empty())
 		return loadImage(locator);
 	else
+	{
+		logGlobal->error("Failed to load non-existing image");
 		return loadImage(ImageLocator(ImagePath::builtin("DEFAULT"), mode));
+	}
 }
 
 std::shared_ptr<IImage> RenderHandler::loadImage(const ImagePath & path, EImageBlitMode mode)
diff --git a/client/renderSDL/ScalableImage.cpp b/client/renderSDL/ScalableImage.cpp
index 93ec8d803..40a22c722 100644
--- a/client/renderSDL/ScalableImage.cpp
+++ b/client/renderSDL/ScalableImage.cpp
@@ -97,8 +97,10 @@ void ScalableImageParameters::preparePalette(const SDL_Palette * originalPalette
 {
 	switch(blitMode)
 	{
-		case EImageBlitMode::ONLY_SHADOW:
-		case EImageBlitMode::ONLY_OVERLAY:
+		case EImageBlitMode::ONLY_SHADOW_HIDE_FLAG_COLOR:
+		case EImageBlitMode::ONLY_SHADOW_HIDE_SELECTION:
+		case EImageBlitMode::ONLY_FLAG_COLOR:
+		case EImageBlitMode::ONLY_SELECTION:
 			adjustPalette(originalPalette, blitMode, ColorFilter::genAlphaShifter(0), 0);
 			break;
 	}
@@ -107,37 +109,49 @@ void ScalableImageParameters::preparePalette(const SDL_Palette * originalPalette
 	{
 		case EImageBlitMode::SIMPLE:
 		case EImageBlitMode::WITH_SHADOW:
-		case EImageBlitMode::ONLY_SHADOW:
-		case EImageBlitMode::WITH_SHADOW_AND_OVERLAY:
+		case EImageBlitMode::ONLY_SHADOW_HIDE_FLAG_COLOR:
+		case EImageBlitMode::ONLY_SHADOW_HIDE_SELECTION:
+		case EImageBlitMode::WITH_SHADOW_AND_FLAG_COLOR:
+		case EImageBlitMode::WITH_SHADOW_AND_SELECTION:
 			setShadowTransparency(originalPalette, 1.0);
 			break;
-		case EImageBlitMode::ONLY_BODY:
+		case EImageBlitMode::ONLY_BODY_HIDE_SELECTION:
+		case EImageBlitMode::ONLY_BODY_HIDE_FLAG_COLOR:
 		case EImageBlitMode::ONLY_BODY_IGNORE_OVERLAY:
-		case EImageBlitMode::ONLY_OVERLAY:
+		case EImageBlitMode::ONLY_FLAG_COLOR:
+		case EImageBlitMode::ONLY_SELECTION:
 			setShadowTransparency(originalPalette, 0.0);
 			break;
 	}
 
 	switch(blitMode)
 	{
-		case EImageBlitMode::ONLY_OVERLAY:
-		case EImageBlitMode::WITH_SHADOW_AND_OVERLAY:
-			setOverlayColor(originalPalette, Colors::WHITE_TRUE);
+		case EImageBlitMode::ONLY_FLAG_COLOR:
+		case EImageBlitMode::WITH_SHADOW_AND_FLAG_COLOR:
+			setOverlayColor(originalPalette, Colors::WHITE_TRUE, false);
 			break;
-		case EImageBlitMode::ONLY_SHADOW:
-		case EImageBlitMode::ONLY_BODY:
-			setOverlayColor(originalPalette, Colors::TRANSPARENCY);
+		case EImageBlitMode::ONLY_SELECTION:
+		case EImageBlitMode::WITH_SHADOW_AND_SELECTION:
+			setOverlayColor(originalPalette, Colors::WHITE_TRUE, true);
+			break;
+		case EImageBlitMode::ONLY_SHADOW_HIDE_FLAG_COLOR:
+		case EImageBlitMode::ONLY_BODY_HIDE_FLAG_COLOR:
+			setOverlayColor(originalPalette, Colors::TRANSPARENCY, false);
+			break;
+		case EImageBlitMode::ONLY_SHADOW_HIDE_SELECTION:
+		case EImageBlitMode::ONLY_BODY_HIDE_SELECTION:
+			setOverlayColor(originalPalette, Colors::TRANSPARENCY, true);
 			break;
 	}
 }
 
-void ScalableImageParameters::setOverlayColor(const SDL_Palette * originalPalette, const ColorRGBA & color)
+void ScalableImageParameters::setOverlayColor(const SDL_Palette * originalPalette, const ColorRGBA & color, bool includeShadow)
 {
 	palette->colors[5] = CSDL_Ext::toSDL(addColors(targetPalette[5], color));
 
-	for (int i : {6,7})
+	if (includeShadow)
 	{
-		if (colorsSimilar(originalPalette->colors[i], sourcePalette[i]))
+		for (int i : {6,7})
 			palette->colors[i] = CSDL_Ext::toSDL(addColors(targetPalette[i], color));
 	}
 }
@@ -183,7 +197,7 @@ void ScalableImageParameters::setShadowTransparency(const SDL_Palette * original
 void ScalableImageParameters::adjustPalette(const SDL_Palette * originalPalette, EImageBlitMode blitMode, const ColorFilter & shifter, uint32_t colorsToSkipMask)
 {
 	// If shadow is enabled, following colors must be skipped unconditionally
-	if (blitMode == EImageBlitMode::WITH_SHADOW || blitMode == EImageBlitMode::WITH_SHADOW_AND_OVERLAY)
+	if (blitMode == EImageBlitMode::WITH_SHADOW || blitMode == EImageBlitMode::WITH_SHADOW_AND_SELECTION || blitMode == EImageBlitMode::WITH_SHADOW_AND_FLAG_COLOR)
 		colorsToSkipMask |= (1 << 0) + (1 << 1) + (1 << 4);
 
 	// Note: here we skip first colors in the palette that are predefined in H3 images
@@ -353,7 +367,7 @@ void ScalableImageInstance::setOverlayColor(const ColorRGBA & color)
 	parameters.ovelayColorMultiplier = color;
 
 	if (parameters.palette)
-		parameters.setOverlayColor(image->getPalette(), color);
+		parameters.setOverlayColor(image->getPalette(), color, blitMode == EImageBlitMode::WITH_SHADOW_AND_SELECTION);
 }
 
 void ScalableImageInstance::playerColored(const PlayerColor & player)
@@ -414,7 +428,7 @@ std::shared_ptr<const ISharedImage> ScalableImageShared::loadOrGenerateImage(EIm
 	{
 		// optional images for 1x resolution - only try load them, don't attempt to generate
 		// this block should never be called for 'body' layer - that image is loaded unconditionally before construction
-		assert(mode == EImageBlitMode::ONLY_SHADOW || mode == EImageBlitMode::ONLY_OVERLAY || color != PlayerColor::CANNOT_DETERMINE);
+		assert(mode == EImageBlitMode::ONLY_SHADOW_HIDE_FLAG_COLOR || mode == EImageBlitMode::ONLY_SHADOW_HIDE_SELECTION || mode == EImageBlitMode::ONLY_FLAG_COLOR || mode == EImageBlitMode::ONLY_SELECTION || color != PlayerColor::CANNOT_DETERMINE);
 		return nullptr;
 	}
 
@@ -427,7 +441,7 @@ std::shared_ptr<const ISharedImage> ScalableImageShared::loadOrGenerateImage(EIm
 		{
 			if (scaling == 1)
 			{
-				if (mode == EImageBlitMode::ONLY_SHADOW || mode == EImageBlitMode::ONLY_OVERLAY || color != PlayerColor::CANNOT_DETERMINE)
+				if (mode == EImageBlitMode::ONLY_SHADOW_HIDE_FLAG_COLOR || mode == EImageBlitMode::ONLY_SHADOW_HIDE_SELECTION || mode == EImageBlitMode::ONLY_FLAG_COLOR || mode == EImageBlitMode::ONLY_SELECTION || color != PlayerColor::CANNOT_DETERMINE)
 				{
 					ScalableImageParameters parameters(getPalette(), mode);
 					return loadedImage->scaleInteger(scalingFactor, parameters.palette, mode);
@@ -464,9 +478,13 @@ void ScalableImageShared::loadScaledImages(int8_t scalingFactor, PlayerColor col
 				scaled[scalingFactor].body[0] = loadOrGenerateImage(locator.layer, scalingFactor, PlayerColor::CANNOT_DETERMINE, scaled[1].body[0]);
 				break;
 
-			case EImageBlitMode::WITH_SHADOW_AND_OVERLAY:
-			case EImageBlitMode::ONLY_BODY:
-				scaled[scalingFactor].body[0] = loadOrGenerateImage(EImageBlitMode::ONLY_BODY, scalingFactor, PlayerColor::CANNOT_DETERMINE, scaled[1].body[0]);
+			case EImageBlitMode::WITH_SHADOW_AND_SELECTION:
+			case EImageBlitMode::ONLY_BODY_HIDE_SELECTION:
+				scaled[scalingFactor].body[0] = loadOrGenerateImage(EImageBlitMode::ONLY_BODY_HIDE_SELECTION, scalingFactor, PlayerColor::CANNOT_DETERMINE, scaled[1].body[0]);
+				break;
+			case EImageBlitMode::WITH_SHADOW_AND_FLAG_COLOR:
+			case EImageBlitMode::ONLY_BODY_HIDE_FLAG_COLOR:
+				scaled[scalingFactor].body[0] = loadOrGenerateImage(EImageBlitMode::ONLY_BODY_HIDE_FLAG_COLOR, scalingFactor, PlayerColor::CANNOT_DETERMINE, scaled[1].body[0]);
 				break;
 
 			case EImageBlitMode::WITH_SHADOW:
@@ -486,9 +504,13 @@ void ScalableImageShared::loadScaledImages(int8_t scalingFactor, PlayerColor col
 				scaled[scalingFactor].playerColored[1+color.getNum()] = loadOrGenerateImage(locator.layer, scalingFactor, color, scaled[1].playerColored[1+color.getNum()]);
 				break;
 
-			case EImageBlitMode::WITH_SHADOW_AND_OVERLAY:
-			case EImageBlitMode::ONLY_BODY:
-				scaled[scalingFactor].playerColored[1+color.getNum()] = loadOrGenerateImage(EImageBlitMode::ONLY_BODY, scalingFactor, color, scaled[1].playerColored[1+color.getNum()]);
+			case EImageBlitMode::WITH_SHADOW_AND_SELECTION:
+			case EImageBlitMode::ONLY_BODY_HIDE_SELECTION:
+				scaled[scalingFactor].playerColored[1+color.getNum()] = loadOrGenerateImage(EImageBlitMode::ONLY_BODY_HIDE_SELECTION, scalingFactor, color, scaled[1].playerColored[1+color.getNum()]);
+				break;
+			case EImageBlitMode::WITH_SHADOW_AND_FLAG_COLOR:
+			case EImageBlitMode::ONLY_BODY_HIDE_FLAG_COLOR:
+				scaled[scalingFactor].playerColored[1+color.getNum()] = loadOrGenerateImage(EImageBlitMode::ONLY_BODY_HIDE_FLAG_COLOR, scalingFactor, color, scaled[1].playerColored[1+color.getNum()]);
 				break;
 
 			case EImageBlitMode::WITH_SHADOW:
@@ -503,9 +525,13 @@ void ScalableImageShared::loadScaledImages(int8_t scalingFactor, PlayerColor col
 		switch(locator.layer)
 		{
 			case EImageBlitMode::WITH_SHADOW:
-			case EImageBlitMode::ONLY_SHADOW:
-			case EImageBlitMode::WITH_SHADOW_AND_OVERLAY:
-				scaled[scalingFactor].shadow[0] = loadOrGenerateImage(EImageBlitMode::ONLY_SHADOW, scalingFactor, PlayerColor::CANNOT_DETERMINE, scaled[1].shadow[0]);
+			case EImageBlitMode::ONLY_SHADOW_HIDE_SELECTION:
+			case EImageBlitMode::WITH_SHADOW_AND_SELECTION:
+				scaled[scalingFactor].shadow[0] = loadOrGenerateImage(EImageBlitMode::ONLY_SHADOW_HIDE_SELECTION, scalingFactor, PlayerColor::CANNOT_DETERMINE, scaled[1].shadow[0]);
+				break;
+			case EImageBlitMode::ONLY_SHADOW_HIDE_FLAG_COLOR:
+			case EImageBlitMode::WITH_SHADOW_AND_FLAG_COLOR:
+				scaled[scalingFactor].shadow[0] = loadOrGenerateImage(EImageBlitMode::ONLY_SHADOW_HIDE_FLAG_COLOR, scalingFactor, PlayerColor::CANNOT_DETERMINE, scaled[1].shadow[0]);
 				break;
 			default:
 				break;
@@ -516,9 +542,13 @@ void ScalableImageShared::loadScaledImages(int8_t scalingFactor, PlayerColor col
 	{
 		switch(locator.layer)
 		{
-			case EImageBlitMode::ONLY_OVERLAY:
-			case EImageBlitMode::WITH_SHADOW_AND_OVERLAY:
-				scaled[scalingFactor].overlay[0] = loadOrGenerateImage(EImageBlitMode::ONLY_OVERLAY, scalingFactor, PlayerColor::CANNOT_DETERMINE, scaled[1].overlay[0]);
+			case EImageBlitMode::ONLY_FLAG_COLOR:
+			case EImageBlitMode::WITH_SHADOW_AND_FLAG_COLOR:
+				scaled[scalingFactor].overlay[0] = loadOrGenerateImage(EImageBlitMode::ONLY_FLAG_COLOR, scalingFactor, PlayerColor::CANNOT_DETERMINE, scaled[1].overlay[0]);
+				break;
+			case EImageBlitMode::ONLY_SELECTION:
+			case EImageBlitMode::WITH_SHADOW_AND_SELECTION:
+				scaled[scalingFactor].overlay[0] = loadOrGenerateImage(EImageBlitMode::ONLY_SELECTION, scalingFactor, PlayerColor::CANNOT_DETERMINE, scaled[1].overlay[0]);
 				break;
 			default:
 				break;
diff --git a/client/renderSDL/ScalableImage.h b/client/renderSDL/ScalableImage.h
index d662f9a01..7578130c2 100644
--- a/client/renderSDL/ScalableImage.h
+++ b/client/renderSDL/ScalableImage.h
@@ -39,7 +39,7 @@ struct ScalableImageParameters : boost::noncopyable
 	void setShadowTransparency(const SDL_Palette * originalPalette, float factor);
 	void shiftPalette(const SDL_Palette * originalPalette, uint32_t firstColorID, uint32_t colorsToMove, uint32_t distanceToMove);
 	void playerColored(PlayerColor player);
-	void setOverlayColor(const SDL_Palette * originalPalette, const ColorRGBA & color);
+	void setOverlayColor(const SDL_Palette * originalPalette, const ColorRGBA & color, bool includeShadow);
 	void preparePalette(const SDL_Palette * originalPalette, EImageBlitMode blitMode);
 	void adjustPalette(const SDL_Palette * originalPalette, EImageBlitMode blitMode, const ColorFilter & shifter, uint32_t colorsToSkipMask);
 };
diff --git a/client/widgets/Images.cpp b/client/widgets/Images.cpp
index 6693300e3..4757e5736 100644
--- a/client/widgets/Images.cpp
+++ b/client/widgets/Images.cpp
@@ -195,12 +195,12 @@ CAnimImage::CAnimImage(const AnimationPath & name, size_t Frame, size_t Group, i
 {
 	pos.x += x;
 	pos.y += y;
-	anim = GH.renderHandler().loadAnimation(name, (Flags & CCreatureAnim::CREATURE_MODE) ? EImageBlitMode::WITH_SHADOW_AND_OVERLAY : EImageBlitMode::COLORKEY);
+	anim = GH.renderHandler().loadAnimation(name, (Flags & CCreatureAnim::CREATURE_MODE) ? EImageBlitMode::WITH_SHADOW_AND_SELECTION: EImageBlitMode::COLORKEY);
 	init();
 }
 
 CAnimImage::CAnimImage(const AnimationPath & name, size_t Frame, Rect targetPos, size_t Group, ui8 Flags):
-	anim(GH.renderHandler().loadAnimation(name, (Flags & CCreatureAnim::CREATURE_MODE) ? EImageBlitMode::WITH_SHADOW_AND_OVERLAY : EImageBlitMode::COLORKEY)),
+	anim(GH.renderHandler().loadAnimation(name, (Flags & CCreatureAnim::CREATURE_MODE) ? EImageBlitMode::WITH_SHADOW_AND_SELECTION : EImageBlitMode::COLORKEY)),
 	frame(Frame),
 	group(Group),
 	flags(Flags),
@@ -318,7 +318,7 @@ bool CAnimImage::isPlayerColored() const
 }
 
 CShowableAnim::CShowableAnim(int x, int y, const AnimationPath & name, ui8 Flags, ui32 frameTime, size_t Group, uint8_t alpha):
-	anim(GH.renderHandler().loadAnimation(name, (Flags & CREATURE_MODE) ? EImageBlitMode::WITH_SHADOW_AND_OVERLAY : EImageBlitMode::COLORKEY)),
+	anim(GH.renderHandler().loadAnimation(name, (Flags & CREATURE_MODE) ? EImageBlitMode::WITH_SHADOW_AND_SELECTION : EImageBlitMode::COLORKEY)),
 	group(Group),
 	frame(0),
 	first(0),