From 35452346386a8a4bdcc44d5d9155c34287c93fae Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 6 Dec 2025 22:46:42 +0100 Subject: [PATCH] HD Edition support --- CI/before_install/linux_qt5.sh | 3 +- CI/before_install/linux_qt6.sh | 3 +- CI/install_conan_dependencies.sh | 2 +- CMakeLists.txt | 2 + client/CMakeLists.txt | 8 +- client/render/CDefFile.cpp | 63 +++++-- client/render/CDefFile.h | 13 +- client/render/hdEdition/DdsFormat.cpp | 165 +++++++++++++++++++ client/render/hdEdition/DdsFormat.h | 84 ++++++++++ client/render/hdEdition/HdImageLoader.cpp | 191 ++++++++++++++++++++++ client/render/hdEdition/HdImageLoader.h | 45 +++++ client/render/hdEdition/PakLoader.cpp | 151 +++++++++++++++++ client/render/hdEdition/PakLoader.h | 68 ++++++++ client/renderSDL/RenderHandler.cpp | 75 ++++++++- client/renderSDL/RenderHandler.h | 6 + client/renderSDL/SDLImage.cpp | 10 ++ client/renderSDL/SDLImage.h | 3 + client/renderSDL/SDL_Extensions.cpp | 87 ++++++++++ client/renderSDL/SDL_Extensions.h | 3 + cmake_modules/Findlibsquish.cmake | 44 +++++ dependencies | 2 +- docs/developers/Building_Linux.md | 4 +- launcher/CMakeLists.txt | 2 + launcher/modManager/cmodlistview_moc.cpp | 17 ++ launcher/modManager/cmodlistview_moc.h | 3 + launcher/modManager/hdextractor.cpp | 154 +++++++++++++++++ launcher/modManager/hdextractor.h | 76 +++++++++ launcher/startGame/StartGameTab.cpp | 35 ++++ launcher/startGame/StartGameTab.h | 2 + launcher/startGame/StartGameTab.ui | 38 +++++ lib/filesystem/ResourcePath.cpp | 1 + lib/filesystem/ResourcePath.h | 1 + 32 files changed, 1330 insertions(+), 31 deletions(-) create mode 100644 client/render/hdEdition/DdsFormat.cpp create mode 100644 client/render/hdEdition/DdsFormat.h create mode 100644 client/render/hdEdition/HdImageLoader.cpp create mode 100644 client/render/hdEdition/HdImageLoader.h create mode 100644 client/render/hdEdition/PakLoader.cpp create mode 100644 client/render/hdEdition/PakLoader.h create mode 100644 cmake_modules/Findlibsquish.cmake create mode 100644 launcher/modManager/hdextractor.cpp create mode 100644 launcher/modManager/hdextractor.h diff --git a/CI/before_install/linux_qt5.sh b/CI/before_install/linux_qt5.sh index a4bf69ff6..e63c916e8 100644 --- a/CI/before_install/linux_qt5.sh +++ b/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 diff --git a/CI/before_install/linux_qt6.sh b/CI/before_install/linux_qt6.sh index 305ea866d..96af66515 100644 --- a/CI/before_install/linux_qt6.sh +++ b/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 diff --git a/CI/install_conan_dependencies.sh b/CI/install_conan_dependencies.sh index 58b9416bc..a3dfd5d1d 100755 --- a/CI/install_conan_dependencies.sh +++ b/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" diff --git a/CMakeLists.txt b/CMakeLists.txt index ceceb67b1..1948cca63 100644 --- a/CMakeLists.txt +++ b/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) diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 89a61aa63..ac2d74437 100644 --- a/client/CMakeLists.txt +++ b/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) diff --git a/client/render/CDefFile.cpp b/client/render/CDefFile.cpp index 8f6bcf6c4..3f86b8b25 100644 --- a/client/render/CDefFile.cpp +++ b/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 names; + names.reserve(totalEntries); + for (ui32 j = 0; j < totalEntries; j++) + { + std::string n(reinterpret_cast(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(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 >::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(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 CDefFile::getEntries() const diff --git a/client/render/CDefFile.h b/client/render/CDefFile.h index a5c7c0469..30a355d0a 100644 --- a/client/render/CDefFile.h +++ b/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 > offset; + //name[group][frame] - name of frame data in file + std::map > name; std::unique_ptr data; std::unique_ptr 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 getEntries() const; }; diff --git a/client/render/hdEdition/DdsFormat.cpp b/client/render/hdEdition/DdsFormat.cpp new file mode 100644 index 000000000..ba2497ee6 --- /dev/null +++ b/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 +#include + +#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> 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(&magic), 4); + if (magic != FOURCC('D','D','S',' ')) + return nullptr; + + DDSHeader hdr{}; + stream->read(reinterpret_cast(&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 comp(compressedSize); + stream->read(comp.data(), compressedSize); + + rgba = std::make_shared>(w * h * 4); + squish::DecompressImage(rgba->data(), w, h, comp.data(), squishFlags); + + // Insert decoded DDS into cache + { + std::lock_guard 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(w); + int rh = static_cast(h); + + if (rect) + { + rx = std::max(0, rect->x); + ry = std::max(0, rect->y); + rw = std::min(rect->w, static_cast(w) - rx); + rh = std::min(rect->h, static_cast(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(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; +} diff --git a/client/render/hdEdition/DdsFormat.h b/client/render/hdEdition/DdsFormat.h new file mode 100644 index 000000000..63abb2a8f --- /dev/null +++ b/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 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> rgba; + }; + + const size_t DDS_CACHE_MEMORY_CAP = 256 * 1024 * 1024; // MB + size_t currentDDSMemory = 0; + + std::list lruList; // front = most recent + std::mutex cacheMutex; + + struct CacheEntry + { + CachedDDS data; + std::list::iterator lruIt; + }; + + std::unordered_map 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); +}; diff --git a/client/render/hdEdition/HdImageLoader.cpp b/client/render/hdEdition/HdImageLoader.cpp new file mode 100644 index 000000000..e39b7c242 --- /dev/null +++ b/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 +#include + +#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 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 imagesToSkip = { + // skip RoE specific files + "MAINMENU", "GAMSELBK", "GSELPOP1", "SCSELBCK", "LOADGAME", "NEWGAME", "LOADBAR" +}; +const std::unordered_set hdColors = { + // skip colored variants - coloring not supported yet + "_RED", "_BLUE", "_SAND", "_GREEN", "_ORANGE", "_PURPLE", "_BLUEWIN", "_FLESH" +}; + +HdImageLoader::HdImageLoader() + : pakLoader(std::make_shared()) + , ddsFormat(std::make_shared()) + , scalingFactor(ENGINE->screenHandler().getScalingFactor()) + , flagImg({nullptr, nullptr}) +{ + const std::vector> 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(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 tokens; + boost::split(tokens, line, boost::is_space(), boost::token_compress_on); + + std::string key = tokens[0]; + std::vector 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 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(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 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(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> 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(); +} diff --git a/client/render/hdEdition/HdImageLoader.h b/client/render/hdEdition/HdImageLoader.h new file mode 100644 index 000000000..12d82ec1e --- /dev/null +++ b/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; + std::shared_ptr ddsFormat; + std::map> flagData; + void loadFlagData(); + int scalingFactor; + + std::array flagImg; +public: + HdImageLoader(); + ~HdImageLoader(); + + std::shared_ptr getImage(const ImagePath & path, const Point & fullSize, const Point & margins, bool shadow, bool overlay); + std::optional> find(const ImagePath & path); + bool exists(const ImagePath & path); +}; diff --git a/client/render/hdEdition/PakLoader.cpp b/client/render/hdEdition/PakLoader.cpp new file mode 100644 index 000000000..29e4217dc --- /dev/null +++ b/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> stringtoTable(const std::string& input) +{ + std::vector> result; + + std::vector lines; + boost::split(lines, input, boost::is_any_of("\n")); + + for(auto& line : lines) + { + boost::trim(line); + if(line.empty()) + continue; + std::vector 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 & 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 animToSkip, std::unordered_set imagesToSkip, std::unordered_set suffixesToSkip) +{ + auto file = CResourceHandler::get()->load(path); + CBinaryReader reader(file.get()); + + std::vector 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(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(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); + } +} diff --git a/client/render/hdEdition/PakLoader.h b/client/render/hdEdition/PakLoader.h new file mode 100644 index 000000000..841aa3d73 --- /dev/null +++ b/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 sheets; + std::vector images; + }; + + void loadPak(ResourcePath path, int scale, std::unordered_set animToSkip, std::unordered_set imagesToSkip, std::unordered_set suffixesToSkip); + std::map> content; + // Fast lookup: imageName -> (ResourcePath, ImageEntry*, ArchiveEntry*) + std::unordered_map>> imagesByName; +}; diff --git a/client/renderSDL/RenderHandler.cpp b/client/renderSDL/RenderHandler.cpp index 7a941343e..cd9b611a7 100644 --- a/client/renderSDL/RenderHandler.cpp +++ b/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 RenderHandler::getAnimationFile(const AnimationPath & auto result = std::make_shared(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 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 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 RenderHandler::loadImageFromFileUncached(const Ima { auto defFile = getAnimationFile(*locator.defFile); if(defFile->hasFrame(locator.defFrame, locator.defGroup)) - return std::make_shared(defFile.get(), locator.defFrame, locator.defGroup); + { + auto img = std::make_shared(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 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; - pathToLoad = *remappedLocator.image; + 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); + + 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 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 RenderHandler::loadScaledImage(const ImageLocato std::shared_ptr 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(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(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(imagePath, optimizeImage); - else if(locator.scalingFactor == 1) + if(!img && locator.scalingFactor == 1) img = std::dynamic_pointer_cast(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(); // needs to initialize after class construction because we need loaded screenHandler for getScalingFactor() + assert(animationLayouts.empty()); assetGenerator->initialize(); updateGeneratedAssets(); diff --git a/client/renderSDL/RenderHandler.h b/client/renderSDL/RenderHandler.h index 492f1dcd2..62c8351dd 100644 --- a/client/renderSDL/RenderHandler.h +++ b/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>; + std::map>>> animationSpriteDefs; std::map> animationFiles; std::map animationLayouts; std::map> imageFiles; std::map> fonts; std::shared_ptr assetGenerator; + std::shared_ptr hdImageLoader; + std::pair getAnimationSpriteDef(const AnimationPath & path, int frame, int group); + ImagePath getAnimationFrameName(const AnimationPath & path, int frame, int group); std::shared_ptr getAnimationFile(const AnimationPath & path); AnimationLayoutMap & getAnimationLayout(const AnimationPath & path, int scalingFactor, EImageBlitMode mode); void initFromJson(AnimationLayoutMap & layout, const JsonNode & config, EImageBlitMode mode) const; diff --git a/client/renderSDL/SDLImage.cpp b/client/renderSDL/SDLImage.cpp index b40b13693..c7c04b78e 100644 --- a/client/renderSDL/SDLImage.cpp +++ b/client/renderSDL/SDLImage.cpp @@ -483,6 +483,16 @@ std::shared_ptr 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() { diff --git a/client/renderSDL/SDLImage.h b/client/renderSDL/SDLImage.h index 45ac3f190..77b5b2b29 100644 --- a/client/renderSDL/SDLImage.h +++ b/client/renderSDL/SDLImage.h @@ -77,5 +77,8 @@ public: std::shared_ptr drawShadow(bool doSheer) const; std::shared_ptr drawOutline(const ColorRGBA & color, int thickness) const; + void setMargins(const Point & newMargins); + void setFullSize(const Point & newSize); + friend class SDLImageLoader; }; diff --git a/client/renderSDL/SDL_Extensions.cpp b/client/renderSDL/SDL_Extensions.cpp index 2382a90df..f09cc3b86 100644 --- a/client/renderSDL/SDL_Extensions.cpp +++ b/client/renderSDL/SDL_Extensions.cpp @@ -24,6 +24,7 @@ #include #include +#include #include #include @@ -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(0, h, B, 0, w, B), + [&](const tbb::blocked_range2d& 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(static_cast(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(r * factor)); + g = std::min(255, static_cast(g * factor)); + b = std::min(255, static_cast(b * factor)); + + row[x] = SDL_MapRGBA(surface->format, r, g, b, a); + } + } + + SDL_UnlockSurface(surface); +} diff --git a/client/renderSDL/SDL_Extensions.h b/client/renderSDL/SDL_Extensions.h index d90934926..abcdde9b8 100644 --- a/client/renderSDL/SDL_Extensions.h +++ b/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); } diff --git a/cmake_modules/Findlibsquish.cmake b/cmake_modules/Findlibsquish.cmake new file mode 100644 index 000000000..5217e935b --- /dev/null +++ b/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() diff --git a/dependencies b/dependencies index 1210d49b4..185772414 160000 --- a/dependencies +++ b/dependencies @@ -1 +1 @@ -Subproject commit 1210d49b440150d22d7ec283083feb63b1ef17af +Subproject commit 1857724145d25bcf9910f9a953c9d8452235c9a6 diff --git a/docs/developers/Building_Linux.md b/docs/developers/Building_Linux.md index 53d80b656..5e3f30d68 100644 --- a/docs/developers/Building_Linux.md +++ b/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. diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index b9f759851..a443de202 100644 --- a/launcher/CMakeLists.txt +++ b/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 diff --git a/launcher/modManager/cmodlistview_moc.cpp b/launcher/modManager/cmodlistview_moc.cpp index 4c68ae50f..1e35631dd 100644 --- a/launcher/modManager/cmodlistview_moc.cpp +++ b/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; diff --git a/launcher/modManager/cmodlistview_moc.h b/launcher/modManager/cmodlistview_moc.h index 932af2533..56c153b57 100644 --- a/launcher/modManager/cmodlistview_moc.h +++ b/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(); diff --git a/launcher/modManager/hdextractor.cpp b/launcher/modManager/hdextractor.cpp new file mode 100644 index 000000000..7267d0faa --- /dev/null +++ b/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, 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()); + } +} diff --git a/launcher/modManager/hdextractor.h b/launcher/modManager/hdextractor.h new file mode 100644 index 000000000..4c5473574 --- /dev/null +++ b/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 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 languages = { + {"CH", "chinese"}, + {"CZ", "czech"}, + {"DE", "german"}, + {"EN", "english"}, + {"ES", "spanish"}, + {"FR", "french"}, + {"IT", "italian"}, + {"PL", "polish"}, + {"RU", "russian"} + }; + + const std::map submodnames = { + {X2, "x2"}, + {X3, "x3"}, + {LOC_X2, "x2_loc"}, + {LOC_X3, "x3_loc"} + }; + + QWidget *parent; + + SubModType archiveTypeToSubModType(ArchiveType v); + void createModJson(std::optional submodType, QDir path, QString language); +public: + void installHd(); + + HdExtractor(QWidget *p); +}; diff --git a/launcher/startGame/StartGameTab.cpp b/launcher/startGame/StartGameTab.cpp index 3ebd1c75a..909ba353a 100644 --- a/launcher/startGame/StartGameTab.cpp +++ b/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(); diff --git a/launcher/startGame/StartGameTab.h b/launcher/startGame/StartGameTab.h index 262ae69bf..6aeacacf7 100644 --- a/launcher/startGame/StartGameTab.h +++ b/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(); diff --git a/launcher/startGame/StartGameTab.ui b/launcher/startGame/StartGameTab.ui index d17dd69fb..d8de8340d 100644 --- a/launcher/startGame/StartGameTab.ui +++ b/launcher/startGame/StartGameTab.ui @@ -263,6 +263,44 @@ + + + + 0 + 30 + + + + + 16777215 + 30 + + + + Install HD Edition (Steam) + + + + + + + + 40 + 30 + + + + + 40 + 30 + + + + ? + + + + Qt::Vertical diff --git a/lib/filesystem/ResourcePath.cpp b/lib/filesystem/ResourcePath.cpp index 347c3e9fd..905e265bf 100644 --- a/lib/filesystem/ResourcePath.cpp +++ b/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}, diff --git a/lib/filesystem/ResourcePath.h b/lib/filesystem/ResourcePath.h index 4f4b4e9a1..240fc726c 100644 --- a/lib/filesystem/ResourcePath.h +++ b/lib/filesystem/ResourcePath.h @@ -52,6 +52,7 @@ enum class EResType ARCHIVE_ZIP, ARCHIVE_SND, ARCHIVE_LOD, + ARCHIVE_PAK, PALETTE, SAVEGAME, DIRECTORY,