Browse Source

HD Edition support

Laserlicht 2 weeks ago
parent
commit
3545234638

+ 2 - 1
CI/before_install/linux_qt5.sh

@@ -20,7 +20,8 @@ sudo eatmydata apt -yq --no-install-recommends \
   qtbase5-dev qtbase5-dev-tools qttools5-dev qttools5-dev-tools \
   libqt5svg5-dev \
   ninja-build zlib1g-dev libavformat-dev libswscale-dev libtbb-dev \
-  libluajit-5.1-dev libminizip-dev libfuzzylite-dev libsqlite3-dev
+  libluajit-5.1-dev libminizip-dev libfuzzylite-dev libsqlite3-dev \
+  libsquish-dev
 
 sudo rm -f  "$APT_CACHE/lock" || true
 sudo rm -rf "$APT_CACHE/partial" || true

+ 2 - 1
CI/before_install/linux_qt6.sh

@@ -20,7 +20,8 @@ sudo eatmydata apt -yq --no-install-recommends \
   qt6-base-dev qt6-base-dev-tools qt6-tools-dev qt6-tools-dev-tools \
   qt6-l10n-tools qt6-svg-dev \
   ninja-build zlib1g-dev libavformat-dev libswscale-dev libtbb-dev \
-  libluajit-5.1-dev libminizip-dev libfuzzylite-dev libsqlite3-dev
+  libluajit-5.1-dev libminizip-dev libfuzzylite-dev libsqlite3-dev \
+  libsquish-dev
 
 sudo rm -f  "$APT_CACHE/lock" || true
 sudo rm -rf "$APT_CACHE/partial" || true

+ 1 - 1
CI/install_conan_dependencies.sh

@@ -1,6 +1,6 @@
 #!/usr/bin/env bash
 
-RELEASE_TAG="2025-11-06"
+RELEASE_TAG="2025-12-10"
 FILENAME="$1.tgz"
 DOWNLOAD_URL="https://github.com/vcmi/vcmi-dependencies/releases/download/$RELEASE_TAG/$FILENAME"
 

+ 2 - 0
CMakeLists.txt

@@ -504,6 +504,8 @@ if (ENABLE_CLIENT)
 	if(TARGET SDL2_ttf::SDL2_ttf-static)
 		add_library(SDL2::TTF ALIAS SDL2_ttf::SDL2_ttf-static)
 	endif()
+
+	find_package(libsquish REQUIRED)
 endif()
 
 if(ENABLE_LOBBY)

+ 7 - 1
client/CMakeLists.txt

@@ -103,6 +103,9 @@ set(vcmiclientcommon_SRCS
 	render/Graphics.cpp
 	render/IFont.cpp
 	render/ImageLocator.cpp
+	render/hdEdition/HdImageLoader.cpp
+	render/hdEdition/PakLoader.cpp
+	render/hdEdition/DdsFormat.cpp
 
 	renderSDL/CBitmapFont.cpp
 	renderSDL/CTrueTypeFont.cpp
@@ -329,6 +332,9 @@ set(vcmiclientcommon_HEADERS
 	render/ImageLocator.h
 	render/IRenderHandler.h
 	render/IScreenHandler.h
+	render/hdEdition/HdImageLoader.h
+	render/hdEdition/PakLoader.h
+	render/hdEdition/DdsFormat.h
 
 	renderSDL/CBitmapFont.h
 	renderSDL/CTrueTypeFont.h
@@ -509,7 +515,7 @@ endif()
 target_link_libraries(vcmiclientcommon PRIVATE vcmiservercommon)
 
 target_link_libraries(vcmiclientcommon PUBLIC
-		vcmi SDL2::SDL2 SDL2::Image SDL2::Mixer SDL2::TTF
+		vcmi SDL2::SDL2 SDL2::Image SDL2::Mixer SDL2::TTF libsquish::libsquish
 )
 
 if(ENABLE_VIDEO)

+ 50 - 13
client/render/CDefFile.cpp

@@ -69,8 +69,17 @@ CDefFile::CDefFile(const AnimationPath & Name):
 		it+=12;
 		//8 unknown bytes - skipping
 
-		//13 bytes for name of every frame in this block - not used, skipping
-		it+= 13 * (int)totalEntries;
+		std::vector<std::string> names;
+		names.reserve(totalEntries);
+		for (ui32 j = 0; j < totalEntries; j++)
+		{
+			std::string n(reinterpret_cast<const char*>(data.get() + it), 13);
+			if (auto pos = n.find('\0'); pos != std::string::npos)
+				n.erase(pos);
+			names.push_back(std::move(n));
+			it += 13;
+		}
+		name[blockID] = std::move(names);
 
 		for (ui32 j=0; j<totalEntries; j++)
 		{
@@ -87,17 +96,7 @@ void CDefFile::loadFrame(size_t frame, size_t group, IImageLoader &loader) const
 
 	const ui8 * FDef = data.get() + offset.at(group)[frame];
 
-	const SSpriteDef sd = *reinterpret_cast<const SSpriteDef *>(FDef);
-
-	SSpriteDef sprite;
-
-	sprite.format = read_le_u32(&sd.format);
-	sprite.fullWidth = read_le_u32(&sd.fullWidth);
-	sprite.fullHeight = read_le_u32(&sd.fullHeight);
-	sprite.width = read_le_u32(&sd.width);
-	sprite.height = read_le_u32(&sd.height);
-	sprite.leftMargin = read_le_u32(&sd.leftMargin);
-	sprite.topMargin = read_le_u32(&sd.topMargin);
+	SSpriteDef sprite = getFrameInfo(frame, group);
 
 	ui32 currentOffset = sizeof(SSpriteDef);
 
@@ -245,6 +244,44 @@ bool CDefFile::hasFrame(size_t frame, size_t group) const
 	return true;
 }
 
+std::string CDefFile::getName(size_t frame, size_t group) const
+{
+	std::map<size_t, std::vector <std::string> >::const_iterator it;
+	it = name.find(group);
+	if(it == name.end())
+	{
+		return "";
+	}
+
+	if(frame >= it->second.size())
+	{
+		return "";
+	}
+
+	return name.at(group)[frame];
+}
+
+CDefFile::SSpriteDef CDefFile::getFrameInfo(size_t frame, size_t group) const
+{
+	if(!hasFrame(frame, group))
+		return SSpriteDef();
+
+	const ui8 * FDef = data.get() + offset.at(group)[frame];
+	const SSpriteDef sd = *reinterpret_cast<const SSpriteDef *>(FDef);
+
+	SSpriteDef sprite;
+
+	sprite.format = read_le_u32(&sd.format);
+	sprite.fullWidth = read_le_u32(&sd.fullWidth);
+	sprite.fullHeight = read_le_u32(&sd.fullHeight);
+	sprite.width = read_le_u32(&sd.width);
+	sprite.height = read_le_u32(&sd.height);
+	sprite.leftMargin = read_le_u32(&sd.leftMargin);
+	sprite.topMargin = read_le_u32(&sd.topMargin);
+
+	return sprite;
+}
+
 CDefFile::~CDefFile() = default;
 
 const std::map<size_t, size_t > CDefFile::getEntries() const

+ 11 - 2
client/render/CDefFile.h

@@ -12,6 +12,10 @@
 #include "../../lib/vcmi_endian.h"
 #include "../../lib/filesystem/ResourcePath.h"
 
+VCMI_LIB_NAMESPACE_BEGIN
+class Point;
+VCMI_LIB_NAMESPACE_END
+
 class IImageLoader;
 struct SDL_Color;
 
@@ -19,8 +23,7 @@ struct SDL_Color;
 /// After loading will store general info (palette and frame offsets) and pointer to file itself
 class CDefFile
 {
-private:
-
+public:
 	PACKED_STRUCT_BEGIN
 	struct SSpriteDef
 	{
@@ -33,8 +36,12 @@ private:
 		si32 leftMargin;
 		si32 topMargin;
 	} PACKED_STRUCT_END;
+private:
+
 	//offset[group][frame] - offset of frame data in file
 	std::map<size_t, std::vector <size_t> > offset;
+	//name[group][frame] - name of frame data in file
+	std::map<size_t, std::vector <std::string> > name;
 
 	std::unique_ptr<ui8[]>       data;
 	std::unique_ptr<SDL_Color[]> palette;
@@ -46,6 +53,8 @@ public:
 	//load frame as SDL_Surface
 	void loadFrame(size_t frame, size_t group, IImageLoader &loader) const;
 	bool hasFrame(size_t frame, size_t group) const;
+	std::string getName(size_t frame, size_t group) const;
+	SSpriteDef getFrameInfo(size_t frame, size_t group) const;
 
 	const std::map<size_t, size_t> getEntries() const;
 };

+ 165 - 0
client/render/hdEdition/DdsFormat.cpp

@@ -0,0 +1,165 @@
+/*
+ * DdsFormat.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 "DdsFormat.h"
+
+#include <SDL_surface.h>
+#include <squish.h>
+
+#include "../../../lib/filesystem/CInputStream.h"
+
+void DdsFormat::touchLRU(CacheEntry & e, const std::string & key)
+{
+	lruList.erase(e.lruIt);
+	lruList.push_front(key);
+	e.lruIt = lruList.begin();
+}
+
+void DdsFormat::evictIfNeeded()
+{
+	while (currentDDSMemory > DDS_CACHE_MEMORY_CAP && !lruList.empty())
+	{
+		const std::string &oldKey = lruList.back();
+		auto it = ddsCache.find(oldKey);
+		if (it != ddsCache.end())
+		{
+			currentDDSMemory -= it->second.data.memSize;
+			lruList.pop_back();
+			ddsCache.erase(it);
+		}
+		else
+		{
+			lruList.pop_back();
+		}
+	}
+}
+
+void DdsFormat::insertIntoCache(const std::string & key, const CachedDDS & cd)
+{
+	auto it = ddsCache.find(key);
+	if (it != ddsCache.end())
+	{
+		currentDDSMemory -= it->second.data.memSize;
+		lruList.erase(it->second.lruIt);
+		ddsCache.erase(it);
+	}
+
+	lruList.push_front(key);
+
+	CacheEntry entry;
+	entry.data = cd;
+	entry.lruIt = lruList.begin();
+	ddsCache.emplace(key, entry);
+
+	currentDDSMemory += cd.memSize;
+	evictIfNeeded();
+}
+
+SDL_Surface * DdsFormat::load(CInputStream * stream, const std::string & cacheName, const Rect * rect)
+{
+	const std::string key = cacheName;
+
+	std::shared_ptr<std::vector<uint8_t>> rgba;
+	uint32_t w = 0;
+	uint32_t h = 0;
+
+	// ---------- Check Cache First ----------
+	{
+		std::lock_guard lock(cacheMutex);
+		auto it = ddsCache.find(key);
+
+		if (it != ddsCache.end())
+		{
+			touchLRU(it->second, key);
+
+			w = it->second.data.w;
+			h = it->second.data.h;
+			rgba = it->second.data.rgba;
+
+			// Continue to rectangle extraction
+		}
+	}
+
+	// Only decode DDS if cache miss
+	if (!rgba)
+	{
+		uint32_t magic = 0;
+		stream->read(reinterpret_cast<ui8*>(&magic), 4);
+		if (magic != FOURCC('D','D','S',' '))
+			return nullptr;
+
+		DDSHeader hdr{};
+		stream->read(reinterpret_cast<ui8*>(&hdr), sizeof(hdr));
+
+		w = hdr.width;
+		h = hdr.height;
+
+		uint32_t fourcc = hdr.pixel_format.fourCC;
+		int squishFlags = 0;
+
+		if (fourcc == FOURCC('D','X','T','1'))
+			squishFlags = squish::kDxt1;
+		else if (fourcc == FOURCC('D','X','T','5'))
+			squishFlags = squish::kDxt5;
+		else
+			return nullptr;
+
+		int blockBytes = (fourcc == FOURCC('D','X','T','1')) ? 8 : 16;
+		int blocks = ((w + 3) / 4) * ((h + 3) / 4);
+		int compressedSize = blocks * blockBytes;
+
+		std::vector<uint8_t> comp(compressedSize);
+		stream->read(comp.data(), compressedSize);
+
+		rgba = std::make_shared<std::vector<uint8_t>>(w * h * 4);
+		squish::DecompressImage(rgba->data(), w, h, comp.data(), squishFlags);
+
+		// Insert decoded DDS into cache
+		{
+			std::lock_guard<std::mutex> lock(cacheMutex);
+			CachedDDS cd;
+			cd.w = w;
+			cd.h = h;
+			cd.rgba = rgba;
+			cd.memSize = w * h * 4;
+			insertIntoCache(key, cd);
+		}
+	}
+
+	// ---------- Rectangle extraction ----------
+	int rx = 0;
+	int ry = 0;
+	int rw = static_cast<int>(w);
+	int rh = static_cast<int>(h);
+
+	if (rect)
+	{
+		rx = std::max(0, rect->x);
+		ry = std::max(0, rect->y);
+		rw = std::min(rect->w, static_cast<int>(w) - rx);
+		rh = std::min(rect->h, static_cast<int>(h) - ry);
+
+		if (rw <= 0 || rh <= 0)
+			return nullptr;
+	}
+
+	SDL_Surface* surf = SDL_CreateRGBSurfaceWithFormat(0, rw, rh, 32, SDL_PIXELFORMAT_RGBA32);
+
+	uint8_t* dst = static_cast<uint8_t*>(surf->pixels);
+
+	for (int y = 0; y < rh; ++y)
+	{
+		const uint8_t* srcLine = rgba->data() + ((ry + y) * w + rx) * 4;
+		std::copy_n(srcLine, rw * 4, dst + y * surf->pitch);
+	}
+
+	return surf;
+}

+ 84 - 0
client/render/hdEdition/DdsFormat.h

@@ -0,0 +1,84 @@
+/*
+ * DdsFormat.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
+
+#include "../../../lib/Rect.h"
+
+struct SDL_Surface;
+
+VCMI_LIB_NAMESPACE_BEGIN
+class CInputStream;
+VCMI_LIB_NAMESPACE_END
+
+#define FOURCC(a,b,c,d) (uint32_t(uint8_t(a)) | (uint32_t(uint8_t(b))<<8) | (uint32_t(uint8_t(c))<<16) | (uint32_t(uint8_t(d))<<24))
+
+class DdsFormat
+{
+#pragma pack(push,1)
+	struct DDSPixelFormat
+	{
+		uint32_t size;
+		uint32_t flags;
+		uint32_t fourCC;
+		uint32_t rgbBits;
+		uint32_t rMask;
+		uint32_t gMask;
+		uint32_t bMask;
+		uint32_t aMask;
+	};
+
+	struct DDSHeader
+	{
+		uint32_t size;
+		uint32_t flags;
+		uint32_t height;
+		uint32_t width;
+		uint32_t pitchOrLinearSize;
+		uint32_t depth;
+		uint32_t mipMapCount;
+		std::array<uint32_t, 11> reserved1;
+		DDSPixelFormat pixel_format;
+		uint32_t caps;
+		uint32_t caps2;
+		uint32_t caps3;
+		uint32_t caps4;
+		uint32_t reserved2;
+	};
+#pragma pack(pop)
+
+	struct CachedDDS
+	{
+		uint32_t w;
+		uint32_t h;
+		size_t memSize;
+		std::shared_ptr<std::vector<uint8_t>> rgba;
+	};
+
+	const size_t DDS_CACHE_MEMORY_CAP = 256 * 1024 * 1024; // MB
+	size_t currentDDSMemory = 0;
+
+	std::list<std::string> lruList; // front = most recent
+	std::mutex cacheMutex;
+
+	struct CacheEntry
+	{
+		CachedDDS data;
+		std::list<std::string>::iterator lruIt;
+	};
+
+	std::unordered_map<std::string, CacheEntry> ddsCache;
+
+	void touchLRU(CacheEntry & e, const std::string & key);
+	void evictIfNeeded();
+	void insertIntoCache(const std::string & key, const CachedDDS & cd);
+
+public:
+	SDL_Surface * load(CInputStream * stream, const std::string & cacheName, const Rect * rect = nullptr);
+};

+ 191 - 0
client/render/hdEdition/HdImageLoader.cpp

@@ -0,0 +1,191 @@
+/*
+ * HdImageLoader.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 "HdImageLoader.h"
+#include "PakLoader.h"
+#include "DdsFormat.h"
+
+#include <SDL_image.h>
+#include <unordered_set>
+
+#include "../../GameEngine.h"
+#include "../../render/CBitmapHandler.h"
+#include "../../render/IScreenHandler.h"
+#include "../../renderSDL/SDLImage.h"
+#include "../../renderSDL/SDL_Extensions.h"
+#include "../../../lib/filesystem/ResourcePath.h"
+#include "../../../lib/filesystem/Filesystem.h"
+#include "../../../lib/filesystem/CCompressedStream.h"
+#include "../../../lib/filesystem/CMemoryStream.h"
+
+const std::unordered_set<std::string> animToSkip = {
+	// skip menu buttons (RoE)
+	"MMENUNG", "MMENULG", "MMENUHS", "MMENUCR", "MMENUQT", "GTSINGL", "GTMULTI", "GTCAMPN", "GTTUTOR", "GTBACK", "GTSINGL", "GTMULTI", "GTCAMPN", "GTTUTOR", "GTBACK",
+	// skip dialogbox - coloring not supported yet
+	"DIALGBOX",
+	// skip water + rivers
+	"WATRTL", "LAVATL", "CLRRVR", "MUDRVR", "LAVRVR"
+};
+const std::unordered_set<std::string> imagesToSkip = {
+	// skip RoE specific files
+	"MAINMENU", "GAMSELBK", "GSELPOP1", "SCSELBCK", "LOADGAME", "NEWGAME", "LOADBAR"
+};
+const std::unordered_set<std::string> hdColors = {
+	// skip colored variants - coloring not supported yet
+	"_RED", "_BLUE", "_SAND", "_GREEN", "_ORANGE", "_PURPLE", "_BLUEWIN", "_FLESH"
+};
+
+HdImageLoader::HdImageLoader()
+	: pakLoader(std::make_shared<PakLoader>())
+	, ddsFormat(std::make_shared<DdsFormat>())
+	, scalingFactor(ENGINE->screenHandler().getScalingFactor())
+	, flagImg({nullptr, nullptr})
+{
+	const std::vector<std::pair<int, ResourcePath>> files = {
+		{2, ResourcePath("DATA/bitmap_DXT_com_x2.pak", EResType::ARCHIVE_PAK)},
+		{2, ResourcePath("DATA/bitmap_DXT_loc_x2.pak", EResType::ARCHIVE_PAK)},
+		{2, ResourcePath("DATA/sprite_DXT_com_x2.pak", EResType::ARCHIVE_PAK)},
+		{2, ResourcePath("DATA/sprite_DXT_loc_x2.pak", EResType::ARCHIVE_PAK)},
+		{3, ResourcePath("DATA/bitmap_DXT_com_x3.pak", EResType::ARCHIVE_PAK)},
+		{3, ResourcePath("DATA/bitmap_DXT_loc_x3.pak", EResType::ARCHIVE_PAK)},
+		{3, ResourcePath("DATA/sprite_DXT_com_x3.pak", EResType::ARCHIVE_PAK)},
+		{3, ResourcePath("DATA/sprite_DXT_loc_x3.pak", EResType::ARCHIVE_PAK)}
+	};
+	for(auto & file : files)
+		if(CResourceHandler::get()->existsResource(file.second) && scalingFactor == file.first)
+			pakLoader->loadPak(file.second, file.first, animToSkip, imagesToSkip, hdColors);
+	
+	loadFlagData();
+}
+
+HdImageLoader::~HdImageLoader()
+{
+	if(flagImg[0])
+		SDL_FreeSurface(flagImg[0]);
+	if(flagImg[1])
+		SDL_FreeSurface(flagImg[1]);
+}
+
+void HdImageLoader::loadFlagData()
+{
+	auto res = ResourcePath("DATA/spriteFlagsInfo.txt", EResType::TEXT);
+	if(!CResourceHandler::get()->existsResource(res))
+		return;
+
+	auto data = CResourceHandler::get()->load(res)->readAll();
+	std::string s(reinterpret_cast<const char*>(data.first.get()), data.second);
+	std::istringstream ss(s);
+	std::string line;
+	while (std::getline(ss, line))
+	{
+		boost::algorithm::trim(line);
+		if(line.empty())
+			continue;
+
+		std::vector<std::string> tokens;
+		boost::split(tokens, line, boost::is_space(), boost::token_compress_on);
+
+		std::string key = tokens[0];
+		std::vector<int> values;
+		for (size_t i = 1; i < tokens.size(); ++i)
+			values.push_back(std::stoi(tokens[i]));
+
+		flagData[key] = values;
+	}
+
+	auto flag = scalingFactor == 3 ? "DATA/flags/flag_grey.png" : "DATA/flags/flag_grey_x2.png";
+	flagImg[0] = BitmapHandler::loadBitmap(ImagePath::builtin(flag));
+	CSDL_Ext::adjustBrightness(flagImg[0], 2.5f);
+	flagImg[1] = CSDL_Ext::verticalFlip(flagImg[0]);
+}
+
+std::shared_ptr<SDLImageShared> HdImageLoader::getImage(const ImagePath & path, const Point & fullSize, const Point & margins, bool shadow, bool overlay)
+{
+	auto imageName = path.getName();
+	auto ret = find(path);
+	if(!ret)
+		return nullptr;
+	
+	if(overlay && !flagData.contains(imageName))
+		return nullptr;
+	else if(overlay)
+	{
+		auto surf = CSDL_Ext::newSurface(fullSize * scalingFactor);
+
+		for(int i = 0; i < flagData[imageName][0]; ++i)
+		{
+			bool flagMirror = flagData[imageName][3 + i * 3];
+			CSDL_Ext::blitSurface(flagMirror ? flagImg[1] : flagImg[0], surf, Point(flagData[imageName][1 + i * 3], flagData[imageName][2 + i * 3]) * scalingFactor);
+		}
+
+		auto img = std::make_shared<SDLImageShared>(surf);
+
+		SDL_FreeSurface(surf);
+
+		return img;
+	}
+
+	auto [res, entry, image] = *ret;
+
+	auto sheetIndex = shadow ? image.shadowSheetIndex : image.sheetIndex;
+	auto sheetOffsetX = shadow ? image.shadowSheetOffsetX : image.sheetOffsetX;
+	auto sheetOffsetY = shadow ? image.shadowSheetOffsetY : image.sheetOffsetY;
+	auto rotation = shadow ? image.shadowRotation : image.rotation;
+	auto width = shadow ? image.shadowWidth : image.width;
+	auto height = shadow ? image.shadowHeight : image.height;
+
+	std::unique_ptr<CInputStream> file = CResourceHandler::get()->load(res);
+	file->seek(entry.metadataOffset);
+	file->skip(entry.metadataSize);
+	for(size_t i = 0; i < sheetIndex; ++i)
+		file->skip(entry.sheets[i].compressedSize);
+
+	CCompressedStream compressedReader(std::move(file), false, entry.sheets[sheetIndex].fullSize);
+	Rect sheetRect(sheetOffsetX, sheetOffsetY, width, height);
+	auto surfCropped = ddsFormat->load(&compressedReader, entry.name + std::to_string(sheetIndex), &sheetRect);
+	SDL_Surface * surfRotated = rotation ? CSDL_Ext::Rotate90(surfCropped) : nullptr;
+
+	auto img = std::make_shared<SDLImageShared>(surfRotated ? surfRotated : surfCropped);
+	if(fullSize.x > 0 && fullSize.y > 0)
+		img->setFullSize(fullSize * scalingFactor);
+	img->setMargins((margins - Point(image.spriteOffsetX, image.spriteOffsetY)) * scalingFactor);
+
+	SDL_FreeSurface(surfCropped);
+	if(surfRotated)
+		SDL_FreeSurface(surfRotated);
+	
+	return img;
+}
+
+std::optional<std::tuple<ResourcePath, PakLoader::ArchiveEntry, PakLoader::ImageEntry>> HdImageLoader::find(const ImagePath & path)
+{
+	const auto targetName = boost::algorithm::to_upper_copy(path.getName());
+	int scale = scalingFactor;
+
+	auto scaleIt = pakLoader->imagesByName.find(scale);
+	if (scaleIt != pakLoader->imagesByName.end())
+	{
+		auto &nameMap = scaleIt->second;
+		auto imageIt = nameMap.find(targetName);
+		if (imageIt != nameMap.end())
+		{
+			auto &[resourcePath, imagePtr, entryPtr] = imageIt->second;
+			return std::make_tuple(resourcePath, *entryPtr, *imagePtr);
+		}
+	}
+
+	return std::nullopt;
+}
+
+bool HdImageLoader::exists(const ImagePath & path)
+{
+	return find(path).has_value();
+}

+ 45 - 0
client/render/hdEdition/HdImageLoader.h

@@ -0,0 +1,45 @@
+/*
+ * HdImageLoader.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
+
+#include "HdImageLoader.h"
+#include "PakLoader.h"
+
+#include "../../../lib/constants/EntityIdentifiers.h"
+#include "../../../lib/filesystem/ResourcePath.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+class Point;
+class PlayerColor;
+VCMI_LIB_NAMESPACE_END
+
+struct SDL_Surface;
+class SDLImageShared;
+class PakLoader;
+class DdsFormat;
+
+class HdImageLoader
+{
+private:
+	std::shared_ptr<PakLoader> pakLoader;
+	std::shared_ptr<DdsFormat> ddsFormat;
+	std::map<std::string, std::vector<int>> flagData;
+	void loadFlagData();
+	int scalingFactor;
+
+	std::array<SDL_Surface *, 2> flagImg;
+public:
+	HdImageLoader();
+	~HdImageLoader();
+
+	std::shared_ptr<SDLImageShared> getImage(const ImagePath & path, const Point & fullSize, const Point & margins, bool shadow, bool overlay);
+	std::optional<std::tuple<ResourcePath, PakLoader::ArchiveEntry, PakLoader::ImageEntry>> find(const ImagePath & path);
+	bool exists(const ImagePath & path);
+};

+ 151 - 0
client/render/hdEdition/PakLoader.cpp

@@ -0,0 +1,151 @@
+/*
+ * PakLoader.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 "PakLoader.h"
+
+#include "../../../lib/filesystem/Filesystem.h"
+#include "../../../lib/filesystem/CBinaryReader.h"
+
+std::vector<std::vector<std::string>> stringtoTable(const std::string& input)
+{
+	std::vector<std::vector<std::string>> result;
+
+	std::vector<std::string> lines;
+	boost::split(lines, input, boost::is_any_of("\n"));
+
+	for(auto& line : lines)
+	{
+		boost::trim(line);
+		if(line.empty())
+			continue;
+		std::vector<std::string> tokens;
+		boost::split(tokens, line, boost::is_any_of(" "), boost::token_compress_on);
+		result.push_back(tokens);
+	}
+
+	return result;
+}
+
+bool endsWithAny(const std::string & s, const std::unordered_set<std::string> & suffixes)
+{
+	for(const auto & suf : suffixes)
+		if(boost::algorithm::ends_with(s, suf))
+			return true;
+
+	return false;
+}
+
+void PakLoader::loadPak(ResourcePath path, int scale, std::unordered_set<std::string> animToSkip, std::unordered_set<std::string> imagesToSkip, std::unordered_set<std::string> suffixesToSkip)
+{
+	auto file = CResourceHandler::get()->load(path);
+	CBinaryReader reader(file.get());
+
+	std::vector<ArchiveEntry> archiveEntries;
+
+	[[maybe_unused]] uint32_t magic = reader.readUInt32();
+	uint32_t headerOffset = reader.readUInt32();
+
+	assert(magic == 4);
+	file->seek(headerOffset);
+
+	uint32_t entriesCount = reader.readUInt32();
+
+	for(uint32_t i = 0; i < entriesCount; ++i)
+	{
+		ArchiveEntry entry;
+
+		std::string buf(20, '\0');
+		reader.read(reinterpret_cast<ui8*>(buf.data()), buf.size());
+		size_t len = buf.find('\0');
+		std::string s = buf.substr(0, len);
+		entry.name = boost::algorithm::to_upper_copy(s);
+		
+		entry.metadataOffset = reader.readUInt32();
+		entry.metadataSize = reader.readUInt32();
+
+		entry.countSheets = reader.readUInt32();
+		entry.compressedSize = reader.readUInt32();
+		entry.fullSize = reader.readUInt32();
+
+		entry.sheets.resize(entry.countSheets);
+
+		for(uint32_t j = 0; j < entry.countSheets; ++j)
+			entry.sheets[j].compressedSize = reader.readUInt32();
+
+		for(uint32_t j = 0; j < entry.countSheets; ++j)
+			entry.sheets[j].fullSize = reader.readUInt32();
+		
+		entry.scale = scale;
+
+		if(animToSkip.find(entry.name) == animToSkip.end() && !endsWithAny(entry.name, suffixesToSkip))
+			archiveEntries.push_back(entry);
+	}
+
+	for(auto & entry : archiveEntries)
+	{
+		file->seek(entry.metadataOffset);
+
+		std::string buf(entry.metadataSize, '\0');
+		reader.read(reinterpret_cast<ui8*>(buf.data()), buf.size());
+		size_t len = buf.find('\0');
+		std::string data = buf.substr(0, len);
+
+		auto table = stringtoTable(data);
+
+		for(const auto & sheet : entry.sheets)
+			reader.skip(sheet.compressedSize);
+
+		ImageEntry image;
+		for(const auto & line : table)
+		{
+			assert(line.size() == 12 || line.size() == 18);
+
+			image.name = boost::algorithm::to_upper_copy(line[0]);
+			image.sheetIndex = std::stol(line[1]);
+			image.spriteOffsetX = std::stol(line[2]);
+			image.unknown1 = std::stol(line[3]);
+			image.spriteOffsetY = std::stol(line[4]);
+			image.unknown2 = std::stol(line[5]);
+			image.sheetOffsetX = std::stol(line[6]);
+			image.sheetOffsetY = std::stol(line[7]);
+			image.width = std::stol(line[8]);
+			image.height = std::stol(line[9]);
+			image.rotation = std::stol(line[10]);
+			image.hasShadow = std::stol(line[11]);
+
+			assert(image.rotation == 0 || image.rotation == 1);
+
+			if(image.hasShadow)
+			{
+				image.shadowSheetIndex = std::stol(line[12]);
+				image.shadowSheetOffsetX = std::stol(line[13]);
+				image.shadowSheetOffsetY = std::stol(line[14]);
+				image.shadowWidth = std::stol(line[15]);
+				image.shadowHeight = std::stol(line[16]);
+				image.shadowRotation = std::stol(line[17]);
+
+				assert(image.shadowRotation == 0 || image.shadowRotation == 1);
+			}
+
+			if(imagesToSkip.find(image.name) == imagesToSkip.end() && !endsWithAny(image.name, suffixesToSkip))
+				entry.images.push_back(image);
+		}
+	}
+
+	content[path] = archiveEntries;
+
+	// Build indices for fast lookup
+	for(auto& entry : content[path])
+	{
+		for(auto& image : entry.images)
+			imagesByName[scale].try_emplace(image.name, path, &image, &entry);
+	}
+}

+ 68 - 0
client/render/hdEdition/PakLoader.h

@@ -0,0 +1,68 @@
+/*
+ * PakLoader.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
+
+#include "PakLoader.h"
+
+#include "../../../lib/filesystem/ResourcePath.h"
+
+class SDLImageShared;
+
+class PakLoader
+{
+public:
+	struct ImageEntry
+	{
+		std::string name;
+		uint32_t sheetIndex = 0;
+		uint32_t spriteOffsetX = 0;
+		uint32_t unknown1 = 0;
+		uint32_t spriteOffsetY = 0;
+		uint32_t unknown2 = 0;
+		uint32_t sheetOffsetX = 0;
+		uint32_t sheetOffsetY = 0;
+		uint32_t width = 0;
+		uint32_t height = 0;
+		uint32_t rotation = 0;
+		uint32_t hasShadow = 0;
+		uint32_t shadowSheetIndex = 0;
+		uint32_t shadowSheetOffsetX = 0;
+		uint32_t shadowSheetOffsetY = 0;
+		uint32_t shadowWidth = 0;
+		uint32_t shadowHeight = 0;
+		uint32_t shadowRotation = 0;
+	};
+	struct SheetEntry
+	{
+		uint32_t compressedSize = 0;
+		uint32_t fullSize = 0;
+	};
+	struct ArchiveEntry
+	{
+		std::string name = "";
+
+		uint32_t metadataOffset = 0;
+		uint32_t metadataSize = 0;
+
+		uint32_t countSheets = 0;
+		uint32_t compressedSize = 0;
+		uint32_t fullSize = 0;
+
+		uint32_t scale = 0;
+
+		std::vector<SheetEntry> sheets;
+		std::vector<ImageEntry> images;
+	};
+
+	void loadPak(ResourcePath path, int scale, std::unordered_set<std::string> animToSkip, std::unordered_set<std::string> imagesToSkip, std::unordered_set<std::string> suffixesToSkip);
+	std::map<ResourcePath, std::vector<ArchiveEntry>> content;
+	// Fast lookup: imageName -> (ResourcePath, ImageEntry*, ArchiveEntry*)
+	std::unordered_map<int, std::unordered_map<std::string, std::tuple<ResourcePath, ImageEntry*, ArchiveEntry*>>> imagesByName;
+};

+ 66 - 9
client/renderSDL/RenderHandler.cpp

@@ -23,6 +23,7 @@
 #include "../render/Colors.h"
 #include "../render/ColorFilter.h"
 #include "../render/IScreenHandler.h"
+#include "../render/hdEdition/HdImageLoader.h"
 
 #include "../../lib/CConfigHandler.h"
 #include "../../lib/CThreadHelper.h"
@@ -70,10 +71,35 @@ std::shared_ptr<CDefFile> RenderHandler::getAnimationFile(const AnimationPath &
 
 	auto result = std::make_shared<CDefFile>(actualPath);
 
+	auto entries = result->getEntries();
+	for(const auto& entry : entries)
+		for(size_t i = 0; i < entry.second; ++i)
+			animationSpriteDefs[actualPath][entry.first][i] = {result->getName(i, entry.first), result->getFrameInfo(i, entry.first)};
+
 	animationFiles[actualPath] = result;
 	return result;
 }
 
+std::pair<std::string, CDefFile::SSpriteDef> RenderHandler::getAnimationSpriteDef(const AnimationPath & path, int frame, int group)
+{
+	AnimationPath actualPath = boost::starts_with(path.getName(), "SPRITES") ? path : path.addPrefix("SPRITES/");
+
+	return animationSpriteDefs[actualPath][group][frame];
+}
+
+ImagePath RenderHandler::getAnimationFrameName(const AnimationPath & path, int frame, int group)
+{
+	auto info = getAnimationSpriteDef(path, frame, group);
+
+	auto frameName = info.first;
+	boost::iterator_range<std::string::iterator> sub = boost::find_first(frameName, ".");
+	if(!sub.empty())
+		frameName = std::string(frameName.begin(), sub.begin());
+	boost::to_upper(frameName);
+
+	return ImagePath::builtin(frameName);
+}
+
 void RenderHandler::initFromJson(AnimationLayoutMap & source, const JsonNode & config, EImageBlitMode mode) const
 {
 	std::string basepath;
@@ -244,7 +270,15 @@ std::shared_ptr<ISharedImage> RenderHandler::loadImageFromFileUncached(const Ima
 	{
 		auto defFile = getAnimationFile(*locator.defFile);
 		if(defFile->hasFrame(locator.defFrame, locator.defGroup))
-			return std::make_shared<SDLImageShared>(defFile.get(), locator.defFrame, locator.defGroup);
+		{
+			auto img = std::make_shared<SDLImageShared>(defFile.get(), locator.defFrame, locator.defGroup);
+
+			auto pathForDefFrame = getAnimationFrameName(*locator.defFile, locator.defFrame, locator.defGroup);
+			if(hdImageLoader->exists(pathForDefFrame))
+				img->setAsyncUpscale(false); // avoids flickering graphics when hd textures are enabled
+
+			return img;
+		}
 		else
 		{
 			logGlobal->error("Frame %d in group %d not found in file: %s", 
@@ -280,15 +314,28 @@ std::shared_ptr<SDLImageShared> RenderHandler::loadScaledImage(const ImageLocato
 	};
 
 	ImagePath pathToLoad;
+	Point defMargins(0, 0);
+	Point defFullSize(0, 0);
 
 	if(locator.defFile)
 	{
 		auto remappedLocator = getLocatorForAnimationFrame(*locator.defFile, locator.defFrame, locator.defGroup, locator.scalingFactor, locator.layer);
 		// we expect that .def's are only used for 1x data, upscaled assets should use standalone images
 		if (!remappedLocator.image)
-			return nullptr;
+		{
+			if(!settings["video"]["useHdTextures"].Bool() || locator.scalingFactor == 1)
+				return nullptr;
+
+			auto info = getAnimationSpriteDef(*locator.defFile, locator.defFrame, locator.defGroup);
+			defMargins = Point(info.second.leftMargin, info.second.topMargin);
+			defFullSize = Point(info.second.fullWidth, info.second.fullHeight);
 
-		pathToLoad = *remappedLocator.image;
+			auto pathForDefFrame = getAnimationFrameName(*locator.defFile, locator.defFrame, locator.defGroup);
+			if(hdImageLoader->exists(pathForDefFrame))
+				pathToLoad = pathForDefFrame;
+		}
+		else
+			pathToLoad = *remappedLocator.image;
 	}
 
 	if(locator.image)
@@ -298,16 +345,19 @@ std::shared_ptr<SDLImageShared> RenderHandler::loadScaledImage(const ImageLocato
 		return nullptr;
 
 	std::string imagePathString = pathToLoad.getName();
+	auto imagePathOriginal = ImagePath::builtin(imagePathString);
 
 	bool generateShadow = locator.generateShadow && (*locator.generateShadow) != SharedImageLocator::ShadowMode::SHADOW_NONE;
 	bool generateOverlay = locator.generateOverlay && (*locator.generateOverlay) != SharedImageLocator::OverlayMode::OVERLAY_NONE;
 	bool isShadow = locator.layer == EImageBlitMode::ONLY_SHADOW_HIDE_SELECTION || locator.layer == EImageBlitMode::ONLY_SHADOW_HIDE_FLAG_COLOR;
 	bool isOverlay = locator.layer == EImageBlitMode::ONLY_FLAG_COLOR || locator.layer == EImageBlitMode::ONLY_SELECTION;
 	bool optimizeImage = !(isShadow && generateShadow) && !(isOverlay && generateOverlay); // images needs to expanded
+	bool overlay = isOverlay && !generateOverlay;
+	bool shadow = isShadow && !generateShadow;
 
-	if(isOverlay && !generateOverlay)
+	if(overlay)
 		imagePathString += "-OVERLAY";
-	if(isShadow && !generateShadow)
+	if(shadow)
 		imagePathString += "-SHADOW";
 	if(locator.playerColored.isValidPlayer())
 		imagePathString += "-" + boost::to_upper_copy(GameConstants::PLAYER_COLOR_NAMES[locator.playerColored.getNum()]);
@@ -320,13 +370,18 @@ std::shared_ptr<SDLImageShared> RenderHandler::loadScaledImage(const ImageLocato
 
 	std::shared_ptr<SDLImageShared> img = nullptr;
 
-	if(CResourceHandler::get()->existsResource(imagePathSprites) && (settings["video"]["useHdTextures"].Bool() || locator.scalingFactor == 1))
+	if(!img && CResourceHandler::get()->existsResource(imagePathSprites) && (settings["video"]["useHdTextures"].Bool() || locator.scalingFactor == 1))
 		img = std::make_shared<SDLImageShared>(imagePathSprites, optimizeImage);
-	else if(CResourceHandler::get()->existsResource(imagePathData) && (settings["video"]["useHdTextures"].Bool() || locator.scalingFactor == 1))
+	if(!img && CResourceHandler::get()->existsResource(imagePathData) && (settings["video"]["useHdTextures"].Bool() || locator.scalingFactor == 1))
 		img = std::make_shared<SDLImageShared>(imagePathData, optimizeImage);
-	else if(CResourceHandler::get()->existsResource(imagePath))
+	if(!img && hdImageLoader->exists(imagePathOriginal) && settings["video"]["useHdTextures"].Bool() && locator.scalingFactor > 1)
+	{
+		if((!isOverlay || !isShadow) || overlay || shadow)
+			img = hdImageLoader->getImage(imagePathOriginal, defFullSize, defMargins, shadow, overlay);
+	}
+	if(!img && CResourceHandler::get()->existsResource(imagePath))
 		img = std::make_shared<SDLImageShared>(imagePath, optimizeImage);
-	else if(locator.scalingFactor == 1)
+	if(!img && locator.scalingFactor == 1)
 		img = std::dynamic_pointer_cast<SDLImageShared>(assetGenerator->generateImage(imagePath));
 
 	if(img)
@@ -503,6 +558,8 @@ static void detectOverlappingBuildings(RenderHandler * renderHandler, const Fact
 
 void RenderHandler::onLibraryLoadingFinished(const Services * services)
 {
+	hdImageLoader = std::make_unique<HdImageLoader>(); // needs to initialize after class construction because we need loaded screenHandler for getScalingFactor()
+
 	assert(animationLayouts.empty());
 	assetGenerator->initialize();
 	updateGeneratedAssets();

+ 6 - 0
client/renderSDL/RenderHandler.h

@@ -10,6 +10,7 @@
 #pragma once
 
 #include "../render/IRenderHandler.h"
+#include "../render/CDefFile.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 class EntityService;
@@ -19,17 +20,22 @@ class CDefFile;
 class SDLImageShared;
 class ScalableImageShared;
 class AssetGenerator;
+class HdImageLoader;
 
 class RenderHandler final : public IRenderHandler
 {
 	using AnimationLayoutMap = std::map<size_t, std::vector<ImageLocator>>;
 
+	std::map<AnimationPath, std::map<int, std::map<int, std::pair<std::string, CDefFile::SSpriteDef>>>> animationSpriteDefs;
 	std::map<AnimationPath, std::weak_ptr<CDefFile>> animationFiles;
 	std::map<AnimationPath, AnimationLayoutMap> animationLayouts;
 	std::map<SharedImageLocator, std::weak_ptr<ScalableImageShared>> imageFiles;
 	std::map<EFonts, std::shared_ptr<const IFont>> fonts;
 	std::shared_ptr<AssetGenerator> assetGenerator;
+	std::shared_ptr<HdImageLoader> hdImageLoader;
 
+	std::pair<std::string, CDefFile::SSpriteDef> getAnimationSpriteDef(const AnimationPath & path, int frame, int group);
+	ImagePath getAnimationFrameName(const AnimationPath & path, int frame, int group);
 	std::shared_ptr<CDefFile> getAnimationFile(const AnimationPath & path);
 	AnimationLayoutMap & getAnimationLayout(const AnimationPath & path, int scalingFactor, EImageBlitMode mode);
 	void initFromJson(AnimationLayoutMap & layout, const JsonNode & config, EImageBlitMode mode) const;

+ 10 - 0
client/renderSDL/SDLImage.cpp

@@ -483,6 +483,16 @@ std::shared_ptr<SDLImageShared> SDLImageShared::drawOutline(const ColorRGBA & co
 	return ret;
 }
 
+void SDLImageShared::setMargins(const Point & newMargins)
+{
+	margins = newMargins;
+}
+
+void SDLImageShared::setFullSize(const Point & newSize)
+{
+	fullSize = newSize;
+}
+
 // Keep the original palette, in order to do color switching operation
 void SDLImageShared::savePalette()
 {

+ 3 - 0
client/renderSDL/SDLImage.h

@@ -77,5 +77,8 @@ public:
 	std::shared_ptr<SDLImageShared> drawShadow(bool doSheer) const;
 	std::shared_ptr<SDLImageShared> drawOutline(const ColorRGBA & color, int thickness) const;
 
+	void setMargins(const Point & newMargins);
+	void setFullSize(const Point & newSize);
+
 	friend class SDLImageLoader;
 };

+ 87 - 0
client/renderSDL/SDL_Extensions.cpp

@@ -24,6 +24,7 @@
 
 #include <tbb/parallel_for.h>
 #include <tbb/parallel_reduce.h>
+#include <tbb/blocked_range2d.h>
 
 #include <SDL_render.h>
 #include <SDL_surface.h>
@@ -165,6 +166,59 @@ SDL_Surface * CSDL_Ext::horizontalFlip(SDL_Surface * toRot)
 	return ret;
 }
 
+SDL_Surface * CSDL_Ext::Rotate90(SDL_Surface * src)
+{
+	if (!src)
+		return nullptr;
+
+	const int w = src->w;
+	const int h = src->h;
+
+	SDL_Surface* dst = SDL_CreateRGBSurfaceWithFormat(0, h, w, src->format->BitsPerPixel, src->format->format);
+	if (!dst)
+		return nullptr;
+
+	SDL_LockSurface(src);
+	SDL_LockSurface(dst);
+
+	const Uint32* srcPixels = (Uint32*)src->pixels;
+	Uint32*       dstPixels = (Uint32*)dst->pixels;
+
+	const int srcPitch = src->pitch / 4;
+	const int dstPitch = dst->pitch / 4;
+
+	constexpr int B = 32; // Tile size (32 is nearly always optimal)
+
+	tbb::parallel_for(
+		tbb::blocked_range2d<int>(0, h, B, 0, w, B),
+		[&](const tbb::blocked_range2d<int>& r)
+		{
+			const int y0 = r.rows().begin();
+			const int y1 = r.rows().end();
+			const int x0 = r.cols().begin();
+			const int x1 = r.cols().end();
+
+			for (int y = y0; y < y1; ++y)
+			{
+				const Uint32* srow = srcPixels + y * srcPitch;
+
+				for (int x = x0; x < x1; ++x)
+				{
+					const int dx = h - 1 - y;
+					const int dy = x;
+
+					dstPixels[dx + dy * dstPitch] = srow[x];
+				}
+			}
+		}
+	);
+
+	SDL_UnlockSurface(src);
+	SDL_UnlockSurface(dst);
+
+	return dst;
+}
+
 uint32_t CSDL_Ext::getPixel(SDL_Surface *surface, const int & x, const int & y, bool colorByte)
 {
 	int bpp = surface->format->BytesPerPixel;
@@ -982,3 +1036,36 @@ SDL_Surface * CSDL_Ext::drawShadow(SDL_Surface * sourceSurface, bool doSheer)
 
 	return destSurface;
 }
+
+void CSDL_Ext::adjustBrightness(SDL_Surface* surface, float factor)
+{
+    if (!surface || surface->format->BytesPerPixel != 4)
+        return;
+
+    SDL_LockSurface(surface);
+
+    Uint8 r;
+	Uint8 g;
+	Uint8 b;
+	Uint8 a;
+
+    for (int y = 0; y < surface->h; y++)
+    {
+        auto* row = reinterpret_cast<Uint32*>(static_cast<Uint8*>(surface->pixels) + y * surface->pitch);
+
+        for (int x = 0; x < surface->w; x++)
+        {
+            Uint32 pixel = row[x];
+
+            SDL_GetRGBA(pixel, surface->format, &r, &g, &b, &a);
+
+            r = std::min(255, static_cast<int>(r * factor));
+            g = std::min(255, static_cast<int>(g * factor));
+            b = std::min(255, static_cast<int>(b * factor));
+
+            row[x] = SDL_MapRGBA(surface->format, r, g, b, a);
+        }
+    }
+
+    SDL_UnlockSurface(surface);
+}

+ 3 - 0
client/renderSDL/SDL_Extensions.h

@@ -49,6 +49,7 @@ SDL_Color toSDL(const ColorRGBA & color);
 
 	SDL_Surface * verticalFlip(SDL_Surface * toRot); //vertical flip
 	SDL_Surface * horizontalFlip(SDL_Surface * toRot); //horizontal flip
+	SDL_Surface * Rotate90(SDL_Surface * src);
 	uint32_t getPixel(SDL_Surface * surface, const int & x, const int & y, bool colorByte = false);
 
 	uint8_t * getPxPtr(const SDL_Surface * const & srf, const int x, const int y);
@@ -79,4 +80,6 @@ SDL_Color toSDL(const ColorRGBA & color);
 
 	SDL_Surface * drawOutline(SDL_Surface * source, const SDL_Color & color, int thickness);
 	SDL_Surface * drawShadow(SDL_Surface * source, bool doSheer);
+
+	void adjustBrightness(SDL_Surface* surface, float factor);
 }

+ 44 - 0
cmake_modules/Findlibsquish.cmake

@@ -0,0 +1,44 @@
+# - Find libsquish
+#
+#  LIBSQUISH_FOUND
+#  LIBSQUISH_INCLUDE_DIR
+#  LIBSQUISH_LIBRARIES
+#
+#  Imported target:
+#     libsquish::libsquish
+
+find_path(
+    LIBSQUISH_INCLUDE_DIR
+    squish.h
+    PATH_SUFFIXES squish
+    PATHS
+        /usr/include
+        /usr/local/include
+)
+
+find_library(
+    LIBSQUISH_LIBRARY
+    NAMES squish libsquish
+    PATHS
+        /usr/lib
+        /usr/local/lib
+    PATH_SUFFIXES
+        lib
+        lib64
+)
+
+set(LIBSQUISH_LIBRARIES ${LIBSQUISH_LIBRARY})
+
+include(FindPackageHandleStandardArgs)
+find_package_handle_standard_args(
+    LibSquish
+    REQUIRED_VARS LIBSQUISH_LIBRARY LIBSQUISH_INCLUDE_DIR
+)
+
+if (LIBSQUISH_FOUND AND NOT TARGET libsquish::libsquish)
+    add_library(libsquish::libsquish UNKNOWN IMPORTED)
+    set_target_properties(libsquish::libsquish PROPERTIES
+        IMPORTED_LOCATION "${LIBSQUISH_LIBRARY}"
+        INTERFACE_INCLUDE_DIRECTORIES "${LIBSQUISH_INCLUDE_DIR}"
+    )
+endif()

+ 1 - 1
dependencies

@@ -1 +1 @@
-Subproject commit 1210d49b440150d22d7ec283083feb63b1ef17af
+Subproject commit 1857724145d25bcf9910f9a953c9d8452235c9a6

+ 2 - 2
docs/developers/Building_Linux.md

@@ -26,7 +26,7 @@ To compile, the following packages (and their development counterparts) are need
 
 For Ubuntu and Debian you need to install this list of packages:
 
-`sudo apt-get install cmake g++ clang libsdl2-dev libsdl2-image-dev libsdl2-ttf-dev libsdl2-mixer-dev zlib1g-dev libavformat-dev libswscale-dev libboost-dev libboost-filesystem-dev libboost-system-dev libboost-thread-dev libboost-program-options-dev libboost-locale-dev libboost-iostreams-dev qtbase5-dev libqt5svg5-dev libtbb-dev libluajit-5.1-dev liblzma-dev libsqlite3-dev libminizip-dev qttools5-dev ninja-build ccache`
+`sudo apt-get install cmake g++ clang libsdl2-dev libsdl2-image-dev libsdl2-ttf-dev libsdl2-mixer-dev zlib1g-dev libavformat-dev libswscale-dev libboost-dev libboost-filesystem-dev libboost-system-dev libboost-thread-dev libboost-program-options-dev libboost-locale-dev libboost-iostreams-dev qtbase5-dev libqt5svg5-dev libtbb-dev libluajit-5.1-dev liblzma-dev libsqlite3-dev libminizip-dev qttools5-dev libsquish-dev ninja-build ccache`
 
 Alternatively if you have VCMI installed from repository or PPA you can use:
 
@@ -34,7 +34,7 @@ Alternatively if you have VCMI installed from repository or PPA you can use:
 
 ### On RPM-based distributions (e.g. Fedora)
 
-`sudo yum install cmake gcc-c++ SDL2-devel SDL2_image-devel SDL2_ttf-devel SDL2_mixer-devel boost boost-devel boost-filesystem boost-system boost-thread boost-program-options boost-locale boost-iostreams zlib-devel ffmpeg-free-devel qt5-qtbase-devel qt5-qtsvg-devel qt5-qttools-devel tbb-devel luajit-devel xz-devel sqlite-devel minizip-devel ccache`
+`sudo yum install cmake gcc-c++ SDL2-devel SDL2_image-devel SDL2_ttf-devel SDL2_mixer-devel boost boost-devel boost-filesystem boost-system boost-thread boost-program-options boost-locale boost-iostreams zlib-devel ffmpeg-free-devel qt5-qtbase-devel qt5-qtsvg-devel qt5-qttools-devel tbb-devel luajit-devel xz-devel sqlite-devel minizip-devel libsquish-devel ccache`
 
 NOTE: VCMI bundles the fuzzylite lib in its source code.
 

+ 2 - 0
launcher/CMakeLists.txt

@@ -15,6 +15,7 @@ set(launcher_SRCS
 		modManager/modstate.cpp
 		modManager/imageviewer_moc.cpp
 		modManager/chroniclesextractor.cpp
+		modManager/hdextractor.cpp
 		settingsView/csettingsview_moc.cpp
 		settingsView/configeditordialog_moc.cpp
 		startGame/StartGameTab.cpp
@@ -53,6 +54,7 @@ set(launcher_HEADERS
 		modManager/modstate.h
 		modManager/imageviewer_moc.h
 		modManager/chroniclesextractor.h
+		modManager/hdextractor.h
 		settingsView/configeditordialog_moc.h
 		settingsView/csettingsview_moc.h
 		startGame/StartGameTab.h

+ 17 - 0
launcher/modManager/cmodlistview_moc.cpp

@@ -1350,6 +1350,8 @@ bool CModListView::isModEnabled(const QString & modName)
 
 bool CModListView::isModInstalled(const QString & modName)
 {
+	if(!modStateModel->isModExists(modName))
+		return false;
 	auto mod = modStateModel->getMod(modName);
 	return mod.isInstalled();
 }
@@ -1373,6 +1375,21 @@ QStringList CModListView::getInstalledChronicles()
 	return result;
 }
 
+bool CModListView::isInstalledHd()
+{
+	for(const auto & modName : modStateModel->getAllMods())
+	{
+		auto mod = modStateModel->getMod(modName);
+		if (!mod.isInstalled())
+			continue;
+
+		if (mod.getID() == "hd-edition")
+			return true;
+	}
+
+	return false;
+}
+
 QStringList CModListView::getUpdateableMods()
 {
 	QStringList result;

+ 3 - 0
launcher/modManager/cmodlistview_moc.h

@@ -102,6 +102,9 @@ public:
 	/// finds all already imported Heroes Chronicles mods (if any)
 	QStringList getInstalledChronicles();
 
+	/// finds imported HD
+	bool isInstalledHd();
+
 	/// finds all mods that can be updated
 	QStringList getUpdateableMods();
 

+ 154 - 0
launcher/modManager/hdextractor.cpp

@@ -0,0 +1,154 @@
+/*
+ * hdextractor.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 "hdextractor.h"
+
+#include "../../lib/VCMIDirs.h"
+
+HdExtractor::HdExtractor(QWidget *p) :
+	parent(p)
+{
+}
+
+HdExtractor::SubModType HdExtractor::archiveTypeToSubModType(ArchiveType v)
+{
+	SubModType subModType = SubModType::X2;
+	if(vstd::contains({ArchiveType::BITMAP_X2, ArchiveType::SPRITE_X2}, v))
+		subModType = SubModType::X2;
+	else if(vstd::contains({ArchiveType::BITMAP_X3, ArchiveType::SPRITE_X3}, v))
+		subModType = SubModType::X3;
+	else if(vstd::contains({ArchiveType::BITMAP_LOC_X2, ArchiveType::SPRITE_LOC_X2}, v))
+		subModType = SubModType::LOC_X2;
+	else if(vstd::contains({ArchiveType::BITMAP_LOC_X3, ArchiveType::SPRITE_LOC_X3}, v))
+		subModType = SubModType::LOC_X3;
+
+	return subModType;
+};
+
+void HdExtractor::installHd()
+{
+	QString tmpDir = QFileDialog::getExistingDirectory(parent, tr("Select Directory with HD Edition (Steam folder)"), QDir::homePath(), QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks);
+	if(tmpDir.isEmpty())
+		return;
+
+	QDir dir(tmpDir);
+
+	if(!dir.exists("HOMM3 2.0.exe"))
+	{
+		QMessageBox::critical(parent, tr("Invalid folder"), tr("The selected folder does not contain HOMM3 2.0.exe! Please select the HD Edition installation folder."));
+		return;
+	}
+
+	QString language = "";
+	auto folderList = QDir(QDir::cleanPath(dir.absolutePath() + QDir::separator() + "data/LOC")).entryList(QDir::Filter::Dirs);
+	for(auto lng : languages.keys())
+		for(auto folder : folderList)
+			if(lng == folder)
+				language = lng;
+	
+	QDir dst(pathToQString(VCMIDirs::get().userDataPath() / "Mods" / "hd-edition"));
+	dst.mkpath(dst.path());
+	createModJson(std::nullopt, dst, language);
+
+	QDir dstData(dst.filePath("content/data"));
+	dstData.mkpath(".");
+	QFile::copy(dir.filePath("data/spriteFlagsInfo.txt"), dstData.filePath("spriteFlagsInfo.txt"));
+
+	QDir dstDataFlags(dst.filePath("content/data/flags"));
+	dstDataFlags.mkpath(".");
+	for (const QFileInfo &fileInfo : QDir(dir.filePath("data/flags")).entryInfoList(QDir::Files))
+	{
+		QString srcFile = fileInfo.absoluteFilePath();
+		QString destFile = dstDataFlags.filePath(fileInfo.fileName());
+		QFile::copy(srcFile, destFile);
+	}
+
+	for(auto modType : {X2, X3, LOC_X2, LOC_X3})
+	{
+		QString suffix = (language.isEmpty() || modType == SubModType::X2 || modType == SubModType::X3) ? "" : "_" + language;
+		QDir modPath = QDir(dst.filePath(QString("mods/") + submodnames.at(modType) + suffix));
+		modPath.mkpath(modPath.path());
+		createModJson(modType, modPath, languages[language]);
+
+		QDir contentDataDir(modPath.filePath("content/data"));
+    	contentDataDir.mkpath(".");
+
+		for(auto & type : {ArchiveType::BITMAP_X2, ArchiveType::BITMAP_X3, ArchiveType::SPRITE_X2, ArchiveType::SPRITE_X3, ArchiveType::BITMAP_LOC_X2, ArchiveType::BITMAP_LOC_X3, ArchiveType::SPRITE_LOC_X2, ArchiveType::SPRITE_LOC_X3})
+		{
+			if(archiveTypeToSubModType(type) != modType)
+				continue;
+
+			QFile fileName;
+			if(vstd::contains({ArchiveType::BITMAP_LOC_X2, ArchiveType::BITMAP_LOC_X3, ArchiveType::SPRITE_LOC_X2, ArchiveType::SPRITE_LOC_X3}, type))
+				fileName.setFileName(dir.filePath(QString("data/LOC/") + language + "/" + pakfiles.at(type)));
+			else
+				fileName.setFileName(dir.filePath(QString("data/") + pakfiles.at(type)));
+
+			QString destPath = contentDataDir.filePath(QFileInfo(fileName).fileName());
+			fileName.copy(contentDataDir.filePath(fileName.fileName()));
+			if(!fileName.copy(destPath))
+				QMessageBox::critical(parent, tr("Extraction error"), tr("Please delete mod and try again! Failed to copy file %1 to %2").arg(fileName.fileName(), destPath), QMessageBox::Ok, QMessageBox::Ok);
+		}
+	}
+}
+
+void HdExtractor::createModJson(std::optional<SubModType> submodType, QDir path, QString language)
+{
+	if (auto result = submodType)
+	{
+		QString scale = (*result == SubModType::X2 || *result == SubModType::LOC_X2) ? "2" : "3";
+		bool isTranslation = (*result == SubModType::LOC_X2 || *result == SubModType::LOC_X3);
+		QJsonObject mod;
+		if(isTranslation)
+		{
+			mod = QJsonObject({
+				{ "modType", "Translation" },
+				{ "name", "HD Localisation (" + language + ") (x" + scale + ")" },
+				{ "description", "Translated Resources (x" + scale + ")" },
+				{ "author", "Ubisoft" },
+				{ "version", "1.0" },
+				{ "contact", "vcmi.eu" },
+				{ "language", language },
+			});
+		}
+		else
+		{
+			mod = QJsonObject({
+				{ "modType", "Graphical" },
+				{ "name", "HD (x" + scale + ")" },
+				{ "description", "Resources (x" + scale + ")" },
+				{ "author", "Ubisoft" },
+				{ "version", "1.0" },
+				{ "contact", "vcmi.eu" },
+			});
+		}
+		
+		QFile jsonFile(path.filePath("mod.json"));
+		jsonFile.open(QFile::WriteOnly);
+		jsonFile.write(QJsonDocument(mod).toJson());
+	}
+	else
+	{
+		QJsonObject mod
+		{
+			{ "modType", "Graphical" },
+			{ "name", "Heroes III HD Edition" },
+			{ "description", "Extracted resources from official Heroes HD to make it usable on VCMI" },
+			{ "author", "Ubisoft" },
+			{ "version", "1.0" },
+			{ "contact", "vcmi.eu" },
+		};
+		
+		QFile jsonFile(path.filePath("mod.json"));
+		jsonFile.open(QFile::WriteOnly);
+		jsonFile.write(QJsonDocument(mod).toJson());
+	}
+}

+ 76 - 0
launcher/modManager/hdextractor.h

@@ -0,0 +1,76 @@
+/*
+ * hdextractor.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
+
+#include "../StdInc.h"
+
+class HdExtractor : public QObject
+{
+	Q_OBJECT
+
+	enum ArchiveType
+	{
+		BITMAP_X2,
+		BITMAP_X3,
+		SPRITE_X2,
+		SPRITE_X3,
+		BITMAP_LOC_X2,
+		BITMAP_LOC_X3,
+		SPRITE_LOC_X2,
+		SPRITE_LOC_X3
+	};
+
+	enum SubModType
+	{
+		X2,
+		X3,
+		LOC_X2,
+		LOC_X3
+	};
+
+	const std::map<ArchiveType, QString> pakfiles = {
+		{BITMAP_X2, "bitmap_DXT_com_x2.pak"},
+		{BITMAP_X3, "bitmap_DXT_com_x3.pak"},
+		{SPRITE_X2, "sprite_DXT_com_x2.pak"},
+		{SPRITE_X3, "sprite_DXT_com_x3.pak"},
+		{BITMAP_LOC_X2, "bitmap_DXT_loc_x2.pak"},
+		{BITMAP_LOC_X3, "bitmap_DXT_loc_x3.pak"},
+		{SPRITE_LOC_X2, "sprite_DXT_loc_x2.pak"},
+		{SPRITE_LOC_X3, "sprite_DXT_loc_x3.pak"}
+	};
+
+	const QMap<QString, QString> languages = {
+		{"CH", "chinese"},
+		{"CZ", "czech"},
+		{"DE", "german"},
+		{"EN", "english"},
+		{"ES", "spanish"},
+		{"FR", "french"},
+		{"IT", "italian"},
+		{"PL", "polish"},
+		{"RU", "russian"}
+	};
+
+	const std::map<SubModType, QString> submodnames = {
+		{X2, "x2"},
+		{X3, "x3"},
+		{LOC_X2, "x2_loc"},
+		{LOC_X3, "x3_loc"}
+	};
+
+	QWidget *parent;
+
+	SubModType archiveTypeToSubModType(ArchiveType v);
+	void createModJson(std::optional<SubModType> submodType, QDir path, QString language);
+public:
+	void installHd();
+
+	HdExtractor(QWidget *p);
+};

+ 35 - 0
launcher/startGame/StartGameTab.cpp

@@ -17,6 +17,7 @@
 #include "../updatedialog_moc.h"
 
 #include "../modManager/cmodlistview_moc.h"
+#include "../modManager/hdextractor.h"
 
 #include "../../lib/filesystem/Filesystem.h"
 #include "../../lib/VCMIDirs.h"
@@ -184,6 +185,14 @@ void StartGameTab::refreshMods()
 	ui->labelChronicles->setText(tr("Heroes Chronicles:\n%n/%1 installed", "", chroniclesMods.size()).arg(chroniclesCount));
 	ui->labelChronicles->setVisible(chroniclesMods.size() != chroniclesCount);
 	ui->buttonChroniclesHelp->setVisible(chroniclesMods.size() != chroniclesCount);
+
+#ifdef VCMI_ANDROID
+	bool hdInstalled = true; // TODO: HD import on android
+#else
+	bool hdInstalled = Helper::getMainWindow()->getModView()->isInstalledHd();
+#endif
+	ui->buttonInstallHdEdition->setVisible(!hdInstalled);
+	ui->buttonInstallHdEditionHelp->setVisible(!hdInstalled);
 }
 
 void StartGameTab::refreshUpdateStatus(EGameUpdateStatus status)
@@ -389,6 +398,32 @@ void StartGameTab::on_buttonMissingCampaignsHelp_clicked()
 	MessageBoxCustom::information(this, ui->labelMissingCampaigns->text(), message);
 }
 
+void StartGameTab::on_buttonInstallHdEditionHelp_clicked()
+{
+	QString message = tr(
+		"You can install resources from official Heroes III HD Edition (Steam) to improve graphics quality in VCMI. "
+		"Choose your Heroes HD folder from Steam.\n\n"
+		"After installation you also have to set an upscale factor > 1 to see HD graphics."
+	);
+	MessageBoxCustom::information(this, ui->buttonInstallHdEdition->text(), message);
+}
+
+void StartGameTab::on_buttonInstallHdEdition_clicked()
+{
+	HdExtractor extractor(this);
+	extractor.installHd();
+
+	QString modName = "hd-edition";
+	auto modView = Helper::getMainWindow()->getModView();
+	
+	modView->reload(modName);
+	if (modView->isModInstalled(modName))
+	{
+		modView->enableModByName(modName);
+		refreshState();
+	}
+}
+
 void StartGameTab::on_buttonPresetExport_clicked()
 {
 	JsonNode presetJson = Helper::getMainWindow()->getModView()->exportCurrentPreset();

+ 2 - 0
launcher/startGame/StartGameTab.h

@@ -63,6 +63,8 @@ private slots:
 	void on_buttonMissingVideoHelp_clicked();
 	void on_buttonMissingFilesHelp_clicked();
 	void on_buttonMissingCampaignsHelp_clicked();
+	void on_buttonInstallHdEditionHelp_clicked();
+	void on_buttonInstallHdEdition_clicked();
 	void on_buttonPresetExport_clicked();
 	void on_buttonPresetImport_clicked();
 	void on_buttonPresetNew_clicked();

+ 38 - 0
launcher/startGame/StartGameTab.ui

@@ -263,6 +263,44 @@
         </widget>
        </item>
        <item row="9" column="0">
+        <widget class="QPushButton" name="buttonInstallHdEdition">
+         <property name="minimumSize">
+          <size>
+           <width>0</width>
+           <height>30</height>
+          </size>
+         </property>
+         <property name="maximumSize">
+          <size>
+           <width>16777215</width>
+           <height>30</height>
+          </size>
+         </property>
+         <property name="text">
+          <string>Install HD Edition (Steam)</string>
+         </property>
+        </widget>
+       </item>
+       <item row="9" column="1">
+        <widget class="QPushButton" name="buttonInstallHdEditionHelp">
+         <property name="minimumSize">
+          <size>
+           <width>40</width>
+           <height>30</height>
+          </size>
+         </property>
+         <property name="maximumSize">
+          <size>
+           <width>40</width>
+           <height>30</height>
+          </size>
+         </property>
+         <property name="text">
+          <string>?</string>
+         </property>
+        </widget>
+       </item>
+       <item row="10" column="0">
         <spacer name="verticalSpacer_2">
          <property name="orientation">
           <enum>Qt::Vertical</enum>

+ 1 - 0
lib/filesystem/ResourcePath.cpp

@@ -122,6 +122,7 @@ EResType EResTypeHelper::getTypeFromExtension(std::string extension)
 		{".PAC",   EResType::ARCHIVE_LOD},
 		{".VID",   EResType::ARCHIVE_VID},
 		{".SND",   EResType::ARCHIVE_SND},
+		{".PAK",   EResType::ARCHIVE_PAK},
 		{".PAL",   EResType::PALETTE},
 		{".VSGM1", EResType::SAVEGAME},
 		{".ERM",   EResType::ERM},

+ 1 - 0
lib/filesystem/ResourcePath.h

@@ -52,6 +52,7 @@ enum class EResType
 	ARCHIVE_ZIP,
 	ARCHIVE_SND,
 	ARCHIVE_LOD,
+	ARCHIVE_PAK,
 	PALETTE,
 	SAVEGAME,
 	DIRECTORY,